feat: SW-276 Implemented child age validation

This commit is contained in:
Hrishikesh Vaipurkar
2024-09-18 13:24:35 +02:00
parent 24f7bc290d
commit a7167dde6a
10 changed files with 110 additions and 54 deletions

View File

@@ -2,6 +2,18 @@ import { z } from "zod"
import type { Location } from "@/types/trpc/routers/hotel/locations"
export const guestRoomsSchema = z.array(
z.object({
adults: z.number().default(1),
children: z.array(
z.object({
age: z.number().nonnegative(),
bed: z.number(),
})
),
})
)
export const bookingWidgetSchema = z.object({
bookingCode: z.string(), // Update this as required when working with booking codes component
date: z.object({
@@ -25,17 +37,7 @@ export const bookingWidgetSchema = z.object({
{ message: "Required" }
),
redemption: z.boolean().default(false),
rooms: z.array(
z.object({
adults: z.number().default(1),
children: z.array(
z.object({
age: z.number(),
bed: z.number(),
})
),
})
),
rooms: guestRoomsSchema,
search: z.string({ coerce: true }).min(1, "Required"),
voucher: z.boolean().default(false),
})

View File

@@ -12,6 +12,7 @@ type ChildSelectorProps = {
index: number
availableBedTypes?: ChildBed[]
updateChild: (child: Child, index: number) => void
childAgeError: boolean
}
export default function ChildInfoSelector({
@@ -23,6 +24,7 @@ export default function ChildInfoSelector({
{ label: "In extra bed", value: 2 },
],
updateChild = (child: Child, index: number) => {},
childAgeError,
}: ChildSelectorProps) {
const intl = useIntl()
const ageLabel = intl.formatMessage({ id: "Age" })
@@ -55,29 +57,38 @@ export default function ChildInfoSelector({
return (
<>
<Select
size={2}
items={ageList}
label={ageLabel}
aria-label={ageLabel}
value={child.age}
onSelect={(key) => {
handleOnSelect(key, "age")
}}
name="age"
/>
{child.age !== -1 ? (
<div>
<Select
items={availableBedTypes}
label={bedLabel}
aria-label={bedLabel}
value={child.bed}
required={true}
items={ageList}
label={ageLabel}
aria-label={ageLabel}
value={child.age}
onSelect={(key) => {
handleOnSelect(key, "bed")
handleOnSelect(key, "age")
}}
name="bed"
name="age"
placeholder={ageLabel}
/>
) : null}
{childAgeError && child.age < 0 ? (
<span>Child Age is required</span>
) : null}
</div>
<div>
{child.age !== -1 ? (
<Select
items={availableBedTypes}
label={bedLabel}
aria-label={bedLabel}
value={child.bed}
onSelect={(key) => {
handleOnSelect(key, "bed")
}}
name="bed"
placeholder={bedLabel}
/>
) : null}
</div>
</>
)
}

View File

@@ -5,8 +5,9 @@
}
.childInfoContainer {
display: flex;
gap: 20px;
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: 1fr 2fr;
}
.textCenter {

View File

@@ -17,6 +17,7 @@ export default function ChildSelector({
roomChildren = [],
adultCount = 1,
updateChildren = (children: Child[]) => {},
childAgeError,
}: ChildSelectorProps) {
const intl = useIntl()
const childrenLabel = intl.formatMessage({ id: "Children" })
@@ -59,19 +60,32 @@ export default function ChildSelector({
}
roomChildren.forEach((child, index) => {
let types: typeof availableBedTypes = []
let selectedBed: boolean = false
if (
child.age <= 5 &&
(availableInAdultsBed > 0 || childInAdultsBedIndices.indexOf(index) != -1)
) {
types.push(availableBedTypes[0])
if (child.bed == 0) {
selectedBed = true
}
}
if (child.age < 3) {
types.push(availableBedTypes[1])
if (child.bed == 1) {
selectedBed = true
}
}
if (child.age > 2) {
types.push(availableBedTypes[2])
if (child.bed == 2) {
selectedBed = true
}
}
childBedTypes[index] = types
if (!selectedBed) {
child.bed = types[0].value
}
})
return (
@@ -93,6 +107,7 @@ export default function ChildSelector({
child={child}
availableBedTypes={childBedTypes[index]}
updateChild={updateChildInfo}
childAgeError={childAgeError}
/>
</div>
))}

View File

@@ -1,5 +1,4 @@
.container {
width: 280px;
display: grid;
gap: var(--Spacing-x2);
padding-bottom: var(--Spacing-x1);

View File

@@ -17,6 +17,7 @@ export default function GuestsRoomPicker({
handleOnSelect = (selected: GuestsRoom, index: number) => {},
room = { adults: 1, children: [] },
index = 1,
childAgeError,
}: GuestsRoomPickerProps) {
const intl = useIntl()
const roomLabel = intl.formatMessage({ id: "Room" })
@@ -41,6 +42,7 @@ export default function GuestsRoomPicker({
roomChildren={room.children}
adultCount={room.adults}
updateChildren={updateChildren}
childAgeError={childAgeError}
/>
</section>
)

View File

@@ -23,6 +23,7 @@ export default function GuestsRoomsPicker({
},
],
closePicker,
childAgeError,
}: GuestsRoomsPickerProps) {
const lang = useLang()
const [selectedGuests, setSelectedGuests] =
@@ -62,26 +63,29 @@ export default function GuestsRoomsPicker({
return (
<>
{selectedGuests.map((room, index) => (
<section key={index}>
<section className={styles.roomContainer} key={index}>
<GuestsRoomPicker
room={room}
handleOnSelect={handleSelectRoomGuests}
index={index}
childAgeError={childAgeError}
/>
<Divider></Divider>
{index > 0 ? (
{/* Not in MVP
{index > 0 ? (
<Button intent="text" onClick={() => removeRoom(index)}>
Remove Room
</Button>
) : null}
) : null} */}
<Divider></Divider>
</section>
))}
<div className={styles.footer}>
{/* Not in MVP
{selectedGuests.length < 4 ? (
<Button intent="text" onClick={addRoom}>
Add Room
</Button>
) : null}
) : null} */}
<Button onClick={closePicker}>Done</Button>
</div>
</>

View File

@@ -6,6 +6,10 @@
overflow: visible;
}
}
.roomContainer {
display: grid;
gap: var(--Spacing-x1);
}
.hideWrapper {
background-color: var(--Main-Grey-White);
border-radius: var(--Corner-radius-Medium);
@@ -14,6 +18,8 @@
position: absolute;
/** BookingWidget padding + border-width */
top: calc(100% + var(--Spacing-x2) + 1px);
width: 360px;
max-width: 100vw; /* for small screens having view port width of 320px */
}
.btn {
background: none;
@@ -28,7 +34,7 @@
}
.footer {
display: grid;
gap: 20px;
grid-template-columns: auto auto;
gap: var(--Spacing-x1);
grid-template-columns: auto;
margin: 10px 0 0;
}

View File

@@ -1,9 +1,10 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { guestRoomsSchema } from "@/components/Forms/BookingWidget/schema"
import Body from "@/components/TempDesignSystem/Text/Body"
import GuestsRoomsPicker from "./GuestsRoomsPicker"
@@ -20,6 +21,7 @@ export default function GuestsRoomsPickerForm({
}: GuestsRoomsFormProps) {
const intl = useIntl()
const [isOpen, setIsOpen] = useState(false)
const [isAgeError, setIsAgeError] = useState(false)
const selectedGuests = useWatch({ name })
const { register, setValue } = useFormContext()
const ref = useRef<HTMLDivElement | null>(null)
@@ -28,19 +30,29 @@ export default function GuestsRoomsPickerForm({
}
function handleSelectGuest(selected: GuestsRoom[]) {
setValue(name, selected)
setIsAgeError(false)
}
const closePicker = useCallback(() => {
const guestRoomsValidData = guestRoomsSchema.safeParse(selectedGuests)
if (guestRoomsValidData.success) {
setIsOpen(false)
} else {
setIsAgeError(true)
}
}, [selectedGuests])
useEffect(() => {
function handleClickOutside(evt: Event) {
const target = evt.target as HTMLElement
if (ref.current && target && !ref.current.contains(target)) {
setIsOpen(false)
closePicker()
}
}
document.addEventListener("click", handleClickOutside)
return () => {
document.removeEventListener("click", handleClickOutside)
}
}, [setIsOpen])
}, [closePicker])
let selectedAdultsCount = 0
let selectedChildrenCount = 0
@@ -51,33 +63,34 @@ export default function GuestsRoomsPickerForm({
})
const selectedRoomsCount = selectedGuests.length
const childCountLabel =
(selectedChildrenCount > 1
selectedChildrenCount > 1
? intl.formatMessage({ id: "Children" })
: intl.formatMessage({ id: "Child" })) + ", "
: intl.formatMessage({ id: "Child" })
return (
<div className={styles.container} data-isopen={isOpen} ref={ref}>
<button className={styles.btn} onClick={handleOnClick} type="button">
<Body className={styles.body}>
{selectedAdultsCount}{" "}
{selectedAdultsCount > 1
? intl.formatMessage({ id: "Adults" })
: intl.formatMessage({ id: "Adult" })}{" "}
{", "}
{selectedChildrenCount > 0
? selectedChildrenCount + " " + childCountLabel
: null}
{selectedRoomsCount}{" "}
{selectedRoomsCount > 1
? intl.formatMessage({ id: "Rooms" })
: intl.formatMessage({ id: "Room" })}
{", "}
{selectedAdultsCount}{" "}
{selectedAdultsCount > 1
? intl.formatMessage({ id: "Adults" })
: intl.formatMessage({ id: "Adult" })}
{selectedChildrenCount > 0
? ", " + selectedChildrenCount + " " + childCountLabel
: null}
</Body>
</button>
<div aria-modal className={styles.hideWrapper} role="dialog">
<GuestsRoomsPicker
handleOnSelect={handleSelectGuest}
initialSelected={selectedGuests}
closePicker={handleOnClick}
closePicker={closePicker}
childAgeError={isAgeError}
/>
</div>
</div>

View File

@@ -21,12 +21,14 @@ export interface GuestsRoomsPickerProps {
handleOnSelect: (selected: GuestsRoom[]) => void
initialSelected?: GuestsRoom[]
closePicker: () => void
childAgeError: boolean
}
export type GuestsRoomPickerProps = {
handleOnSelect: (selected: GuestsRoom, index: number) => void
room: GuestsRoom
index: number
childAgeError: boolean
}
export type AdultSelectorProps = {
@@ -38,4 +40,5 @@ export type ChildSelectorProps = {
roomChildren: Child[]
adultCount: number
updateChildren: (children: Child[]) => void
childAgeError: boolean
}