fix: validation
This commit is contained in:
@@ -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
|
||||||
adults: z.number().default(1),
|
.object({
|
||||||
child: z
|
adults: z.number().default(1),
|
||||||
.array(
|
child: z
|
||||||
z.object({
|
.array(
|
||||||
age: z.number().nonnegative(),
|
z.object({
|
||||||
bed: z.number(),
|
age: z.number().min(0, "Age is required"),
|
||||||
})
|
bed: z.number().min(0, "Bed choice is required"),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.default([]),
|
||||||
|
})
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
const childrenInAdultsBed = value.child.filter(
|
||||||
|
(c) => c.bed === ChildBedMapEnum.IN_ADULTS_BED
|
||||||
)
|
)
|
||||||
.default([]),
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,25 +40,24 @@ export default function GuestsRoomsPickerForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return isDesktop ? (
|
||||||
<>
|
<DialogTrigger onOpenChange={setOverflowClip}>
|
||||||
<DialogTrigger>
|
<Trigger rooms={rooms} className={styles.triggerDesktop} />
|
||||||
<Trigger rooms={rooms} className={styles.triggerMobile} />
|
<Popover placement="bottom start" offset={36}>
|
||||||
<Modal>
|
<Dialog className={styles.pickerContainerDesktop}>
|
||||||
<Dialog className={styles.pickerContainerMobile}>
|
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
||||||
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
</Dialog>
|
||||||
</Dialog>
|
</Popover>
|
||||||
</Modal>
|
</DialogTrigger>
|
||||||
</DialogTrigger>
|
) : (
|
||||||
<DialogTrigger onOpenChange={setOverflowClip}>
|
<DialogTrigger>
|
||||||
<Trigger rooms={rooms} className={styles.triggerDesktop} />
|
<Trigger rooms={rooms} className={styles.triggerMobile} />
|
||||||
<Popover placement="bottom start" offset={36}>
|
<Modal>
|
||||||
<Dialog className={styles.pickerContainerDesktop}>
|
<Dialog className={styles.pickerContainerMobile}>
|
||||||
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Popover>
|
</Modal>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user