sub-task/ SW-695 Prefill Guests data in booking widget

This commit is contained in:
Hrishikesh Vaipurkar
2024-10-25 08:50:23 +02:00
parent 31da31b72d
commit 05d353e224
18 changed files with 342 additions and 182 deletions

View File

@@ -3,13 +3,11 @@ import { serverClient } from "@/lib/trpc/server"
import BookingWidget, { preload } from "@/components/BookingWidget" import BookingWidget, { preload } from "@/components/BookingWidget"
import { BookingWidgetSearchParams } from "@/types/components/bookingWidget" import { BookingWidgetPageProps } from "@/types/components/bookingWidget"
import { LangParams, PageArgs } from "@/types/params"
export default async function BookingWidgetPage({ export default async function BookingWidgetPage({
params,
searchParams, searchParams,
}: PageArgs<LangParams, BookingWidgetSearchParams>) { }: BookingWidgetPageProps) {
if (env.HIDE_FOR_NEXT_RELEASE) { if (env.HIDE_FOR_NEXT_RELEASE) {
return null return null
} }

View File

@@ -9,6 +9,7 @@ import Form from "@/components/Forms/BookingWidget"
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
import { CloseLargeIcon } from "@/components/Icons" import { CloseLargeIcon } from "@/components/Icons"
import { debounce } from "@/utils/debounce" import { debounce } from "@/utils/debounce"
import { getFormattedUrlQueryParams } from "@/utils/url"
import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils" import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils"
import MobileToggleButton from "./MobileToggleButton" import MobileToggleButton from "./MobileToggleButton"
@@ -18,6 +19,7 @@ import styles from "./bookingWidget.module.css"
import type { import type {
BookingWidgetClientProps, BookingWidgetClientProps,
BookingWidgetSchema, BookingWidgetSchema,
BookingWidgetSearchParams,
} from "@/types/components/bookingWidget" } from "@/types/components/bookingWidget"
import type { Location } from "@/types/trpc/routers/hotel/locations" import type { Location } from "@/types/trpc/routers/hotel/locations"
@@ -36,12 +38,14 @@ export default function BookingWidgetClient({
? JSON.parse(sessionStorageSearchData) ? JSON.parse(sessionStorageSearchData)
: undefined : undefined
const bookingWidgetSearchParams = searchParams const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
? new URLSearchParams(searchParams) searchParams
: undefined ? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
const bookingWidgetSearchData = bookingWidgetSearchParams adults: "number",
? getHotelReservationQueryParams(bookingWidgetSearchParams) age: "number",
: undefined bed: "number",
}) as BookingWidgetSearchParams)
: undefined
const getLocationObj = (destination: string): Location | undefined => { const getLocationObj = (destination: string): Location | undefined => {
if (destination) { if (destination) {
@@ -83,7 +87,7 @@ export default function BookingWidgetClient({
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507
// This is specifically to handle timezones falling in different dates. // This is specifically to handle timezones falling in different dates.
fromDate: isDateParamValid fromDate: isDateParamValid
? bookingWidgetSearchData?.fromDate.toString() ? bookingWidgetSearchData?.fromDate?.toString()
: dt().utc().format("YYYY-MM-DD"), : dt().utc().format("YYYY-MM-DD"),
toDate: isDateParamValid toDate: isDateParamValid
? bookingWidgetSearchData?.toDate?.toString() ? bookingWidgetSearchData?.toDate?.toString()
@@ -92,10 +96,10 @@ export default function BookingWidgetClient({
bookingCode: "", bookingCode: "",
redemption: false, redemption: false,
voucher: false, voucher: false,
rooms: [ rooms: bookingWidgetSearchData?.room ?? [
{ {
adults: 1, adults: 1,
children: [], child: [],
}, },
], ],
}, },

View File

@@ -54,6 +54,12 @@ export default function MobileToggleButton({
} }
return acc return acc
}, 0) }, 0)
const totalChildren = rooms.reduce((acc, room) => {
if (room.child) {
acc = acc + room.child.length
}
return acc
}, 0)
return ( return (
<div className={styles.complete} onClick={openMobileSearch} role="button"> <div className={styles.complete} onClick={openMobileSearch} role="button">
<div> <div>
@@ -62,7 +68,7 @@ export default function MobileToggleButton({
{`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage(
{ id: "booking.nights" }, { id: "booking.nights" },
{ totalNights: nights } { totalNights: nights }
)}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${intl.formatMessage({ id: "booking.children" }, { totalChildren })}, ${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
</Caption> </Caption>
</div> </div>
<div className={styles.icon}> <div className={styles.icon}>

View File

@@ -7,6 +7,7 @@ import { dt } from "@/lib/dt"
import DatePicker from "@/components/DatePicker" import DatePicker from "@/components/DatePicker"
import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker" import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker"
import GuestsRoomsProvider from "@/components/GuestsRoomsPicker/Provider/GuestsRoomsProvider"
import { SearchIcon } from "@/components/Icons" import { SearchIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
@@ -29,6 +30,8 @@ export default function FormContent({
const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days") const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days")
const selectedGuests = useWatch({ name: "rooms" })
return ( return (
<> <>
<div className={styles.input}> <div className={styles.input}>
@@ -51,7 +54,9 @@ export default function FormContent({
{rooms} {rooms}
</Caption> </Caption>
</label> </label>
<GuestsRoomsPickerForm /> <GuestsRoomsProvider selectedGuests={selectedGuests}>
<GuestsRoomsPickerForm name="rooms" />
</GuestsRoomsProvider>
</div> </div>
</div> </div>
<div className={styles.voucherContainer}> <div className={styles.voucherContainer}>

View File

@@ -42,7 +42,7 @@ export default function Form({ locations, type }: BookingWidgetFormProps) {
data.rooms.forEach((room, index) => { data.rooms.forEach((room, index) => {
bookingWidgetParams.set(`room[${index}].adults`, room.adults.toString()) bookingWidgetParams.set(`room[${index}].adults`, room.adults.toString())
room.children.forEach((child, childIndex) => { room.child.forEach((child, childIndex) => {
bookingWidgetParams.set( bookingWidgetParams.set(
`room[${index}].child[${childIndex}].age`, `room[${index}].child[${childIndex}].age`,
child.age.toString() child.age.toString()

View File

@@ -4,7 +4,7 @@ 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),
children: z.array( child: z.array(
z.object({ z.object({
age: z.number().nonnegative(), age: z.number().nonnegative(),
bed: z.number(), bed: z.number(),

View File

@@ -21,7 +21,7 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
const intl = useIntl() const intl = useIntl()
const adultsLabel = intl.formatMessage({ id: "Adults" }) const adultsLabel = intl.formatMessage({ id: "Adults" })
const { setValue } = useFormContext() const { setValue } = useFormContext()
const { adults, children, childrenInAdultsBed } = useGuestsRoomsStore( const { adults, child, childrenInAdultsBed } = useGuestsRoomsStore(
(state) => state.rooms[roomIndex] (state) => state.rooms[roomIndex]
) )
const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults) const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults)
@@ -39,13 +39,13 @@ export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
decreaseAdults(roomIndex) decreaseAdults(roomIndex)
setValue(`rooms.${roomIndex}.adults`, adults - 1) setValue(`rooms.${roomIndex}.adults`, adults - 1)
if (childrenInAdultsBed > adults) { if (childrenInAdultsBed > adults) {
const toUpdateIndex = children.findIndex( const toUpdateIndex = child.findIndex(
(child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED (child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED
) )
if (toUpdateIndex != -1) { if (toUpdateIndex != -1) {
setValue( setValue(
`rooms.${roomIndex}.children.${toUpdateIndex}.bed`, `rooms.${roomIndex}.children.${toUpdateIndex}.bed`,
children[toUpdateIndex].age < 3 child[toUpdateIndex].age < 3
? BedTypeEnum.IN_CRIB ? BedTypeEnum.IN_CRIB
: BedTypeEnum.IN_EXTRA_BED : BedTypeEnum.IN_EXTRA_BED
) )

View File

@@ -26,7 +26,7 @@ export default function ChildInfoSelector({
const ageLabel = intl.formatMessage({ id: "Age" }) const ageLabel = intl.formatMessage({ id: "Age" })
const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" }) const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" })
const bedLabel = intl.formatMessage({ id: "Bed" }) const bedLabel = intl.formatMessage({ id: "Bed" })
const { setValue, trigger } = useFormContext() const { setValue } = useFormContext()
const { adults, childrenInAdultsBed } = useGuestsRoomsStore( const { adults, childrenInAdultsBed } = useGuestsRoomsStore(
(state) => state.rooms[roomIndex] (state) => state.rooms[roomIndex]
) )
@@ -51,10 +51,11 @@ export default function ChildInfoSelector({
function updateSelectedAge(age: number) { function updateSelectedAge(age: number) {
updateChildAge(age, roomIndex, index) updateChildAge(age, roomIndex, index)
setValue(`rooms.${roomIndex}.children.${index}.age`, age) setValue(`rooms.${roomIndex}.child.${index}.age`, age, {
shouldValidate: true,
})
const availableBedTypes = getAvailableBeds(age) const availableBedTypes = getAvailableBeds(age)
updateSelectedBed(availableBedTypes[0].value) updateSelectedBed(availableBedTypes[0].value)
trigger("rooms")
} }
function updateSelectedBed(bed: number) { function updateSelectedBed(bed: number) {
@@ -64,7 +65,7 @@ export default function ChildInfoSelector({
decreaseChildInAdultsBed(roomIndex) decreaseChildInAdultsBed(roomIndex)
} }
updateChildBed(bed, roomIndex, index) updateChildBed(bed, roomIndex, index)
setValue(`rooms.${roomIndex}.children.${index}.bed`, bed) setValue(`rooms.${roomIndex}.child.${index}.bed`, bed)
} }
const allBedTypes: ChildBed[] = [ const allBedTypes: ChildBed[] = [
@@ -109,8 +110,9 @@ export default function ChildInfoSelector({
onSelect={(key) => { onSelect={(key) => {
updateSelectedAge(key as number) updateSelectedAge(key as number)
}} }}
name={`rooms.${roomIndex}.children.${index}.age`} name={`rooms.${roomIndex}.child.${index}.age`}
placeholder={ageLabel} placeholder={ageLabel}
maxHeight={150}
/> />
</div> </div>
<div> <div>
@@ -123,7 +125,7 @@ export default function ChildInfoSelector({
onSelect={(key) => { onSelect={(key) => {
updateSelectedBed(key as number) updateSelectedBed(key as number)
}} }}
name={`rooms.${roomIndex}.children.${index}.age`} name={`rooms.${roomIndex}.child.${index}.age`}
placeholder={bedLabel} placeholder={bedLabel}
/> />
) : null} ) : null}

View File

@@ -19,9 +19,7 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
const intl = useIntl() const intl = useIntl()
const childrenLabel = intl.formatMessage({ id: "Children" }) const childrenLabel = intl.formatMessage({ id: "Children" })
const { setValue, trigger } = useFormContext<BookingWidgetSchema>() const { setValue, trigger } = useFormContext<BookingWidgetSchema>()
const children = useGuestsRoomsStore( const children = useGuestsRoomsStore((state) => state.rooms[roomIndex].child)
(state) => state.rooms[roomIndex].children
)
const increaseChildren = useGuestsRoomsStore( const increaseChildren = useGuestsRoomsStore(
(state) => state.increaseChildren (state) => state.increaseChildren
) )
@@ -32,18 +30,22 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
function increaseChildrenCount(roomIndex: number) { function increaseChildrenCount(roomIndex: number) {
if (children.length < 5) { if (children.length < 5) {
increaseChildren(roomIndex) increaseChildren(roomIndex)
setValue(`rooms.${roomIndex}.children.${children.length}`, { setValue(
age: -1, `rooms.${roomIndex}.child.${children.length}`,
bed: -1, {
}) age: -1,
trigger("rooms") bed: -1,
},
{ shouldValidate: true }
)
} }
} }
function decreaseChildrenCount(roomIndex: number) { function decreaseChildrenCount(roomIndex: number) {
if (children.length > 0) { if (children.length > 0) {
const newChildrenList = decreaseChildren(roomIndex) const newChildrenList = decreaseChildren(roomIndex)
setValue(`rooms.${roomIndex}.children`, newChildrenList) setValue(`rooms.${roomIndex}.child`, newChildrenList, {
trigger("rooms") shouldValidate: true,
})
} }
} }

View File

@@ -0,0 +1,26 @@
"use client"
import { PropsWithChildren, useRef } from "react"
import {
GuestsRoomsContext,
type GuestsRoomsStore,
initGuestsRoomsState,
} from "@/stores/guests-rooms"
import { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function GuestsRoomsProvider({
selectedGuests,
children,
}: PropsWithChildren<{ selectedGuests?: GuestsRoom[] }>) {
const initialStore = useRef<GuestsRoomsStore>()
if (!initialStore.current) {
initialStore.current = initGuestsRoomsState(selectedGuests)
}
return (
<GuestsRoomsContext.Provider value={initialStore.current}>
{children}
</GuestsRoomsContext.Provider>
)
}

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useGuestsRoomsStore } from "@/stores/guests-rooms" import { useGuestsRoomsStore } from "@/stores/guests-rooms"
@@ -12,9 +13,14 @@ import GuestsRoomsPicker from "./GuestsRoomsPicker"
import styles from "./guests-rooms-picker.module.css" import styles from "./guests-rooms-picker.module.css"
export default function GuestsRoomsPickerForm() { export default function GuestsRoomsPickerForm({
name = "rooms",
}: {
name: string
}) {
const intl = useIntl() const intl = useIntl()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const { setValue } = useFormContext()
const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore( const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore(
(state) => ({ (state) => ({
rooms: state.rooms, rooms: state.rooms,
@@ -32,10 +38,11 @@ export default function GuestsRoomsPickerForm() {
if (guestRoomsValidData.success) { if (guestRoomsValidData.success) {
setIsOpen(false) setIsOpen(false)
setIsValidated(false) setIsValidated(false)
setValue(name, guestRoomsValidData.data, { shouldValidate: true })
} else { } else {
setIsValidated(true) setIsValidated(true)
} }
}, [rooms, setIsValidated, setIsOpen]) }, [rooms, name, setValue, setIsValidated, setIsOpen])
useEffect(() => { useEffect(() => {
function handleClickOutside(evt: Event) { function handleClickOutside(evt: Event) {

View File

@@ -1,28 +1,12 @@
import { getFormattedUrlQueryParams } from "@/utils/url"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
function getHotelReservationQueryParams(searchParams: URLSearchParams) { function getHotelReservationQueryParams(searchParams: URLSearchParams) {
const searchParamsObject: Record<string, unknown> = Array.from( return getFormattedUrlQueryParams(searchParams, {
searchParams.entries() adults: "number",
).reduce<Record<string, unknown>>( age: "number",
(acc, [key, value]) => { }) as SelectRateSearchParams
const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.'
keys.reduce((nestedAcc, k, i) => {
if (i === keys.length - 1) {
// Convert value to number if the key is 'adults' or 'age'
;(nestedAcc as Record<string, unknown>)[k] =
k === "adults" || k === "age" ? Number(value) : value
} else {
if (!nestedAcc[k]) {
nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array
}
}
return nestedAcc[k] as Record<string, unknown>
}, acc)
return acc
},
{} as Record<string, unknown>
)
return searchParamsObject as SelectRateSearchParams
} }
export default getHotelReservationQueryParams export default getHotelReservationQueryParams

View File

@@ -34,6 +34,7 @@ export default function Select({
required = false, required = false,
tabIndex, tabIndex,
value, value,
maxHeight,
}: SelectProps) { }: SelectProps) {
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined) const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
@@ -81,6 +82,7 @@ export default function Select({
* on the container as well as to not overflow it at any time. * on the container as well as to not overflow it at any time.
*/ */
UNSTABLE_portalContainer={rootDiv} UNSTABLE_portalContainer={rootDiv}
maxHeight={maxHeight}
> >
<ListBox className={styles.listBox}> <ListBox className={styles.listBox}>
{items.map((item) => ( {items.map((item) => (

View File

@@ -9,6 +9,7 @@ export interface SelectProps
onSelect: (key: Key) => void onSelect: (key: Key) => void
placeholder?: string placeholder?: string
value?: string | number value?: string | number
maxHeight?: number
} }
export type SelectPortalContainer = HTMLDivElement | undefined export type SelectPortalContainer = HTMLDivElement | undefined

View File

@@ -1,22 +1,28 @@
"use client" "use client"
import { produce } from "immer" import { produce } from "immer"
import { create } from "zustand" import { createContext, useContext } from "react"
import { create, useStore } from "zustand"
import { BedTypeEnum } from "@/types/components/bookingWidget/enums" import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
import { Child } from "@/types/components/bookingWidget/guestsRoomsPicker" import {
Child,
GuestsRoom,
} from "@/types/components/bookingWidget/guestsRoomsPicker"
interface GuestsRooms { const SESSION_STORAGE_KEY = "guests_rooms"
rooms: [
{ interface extendedGuestsRoom extends GuestsRoom {
adults: number childrenInAdultsBed: number
children: Child[] }
childrenInAdultsBed: number interface GuestsRoomsState {
}, rooms: extendedGuestsRoom[]
]
adultCount: number adultCount: number
childCount: number childCount: number
isValidated: boolean isValidated: boolean
}
interface GuestsRoomsStoreState extends GuestsRoomsState {
increaseAdults: (roomIndex: number) => void increaseAdults: (roomIndex: number) => void
decreaseAdults: (roomIndex: number) => void decreaseAdults: (roomIndex: number) => void
increaseChildren: (roomIndex: number) => void increaseChildren: (roomIndex: number) => void
@@ -30,115 +36,192 @@ interface GuestsRooms {
setIsValidated: (isValidated: boolean) => void setIsValidated: (isValidated: boolean) => void
} }
export const useGuestsRoomsStore = create<GuestsRooms>((set, get) => ({ export function validateBedTypes(data: extendedGuestsRoom[]) {
rooms: [ data.forEach((room) => {
{ room.child.forEach((child) => {
adults: 1, const allowedBedTypes: number[] = []
children: [], if (child.age <= 5 && room.adults >= room.childrenInAdultsBed) {
childrenInAdultsBed: 0, allowedBedTypes.push(BedTypeEnum.IN_ADULTS_BED)
}, } else if (child.age <= 5) {
], room.childrenInAdultsBed = room.childrenInAdultsBed - 1
adultCount: 1, }
childCount: 0, if (child.age < 3) {
isValidated: false, allowedBedTypes.push(BedTypeEnum.IN_CRIB)
increaseAdults: (roomIndex) => }
set( if (child.age > 2) {
produce((state: GuestsRooms) => { allowedBedTypes.push(BedTypeEnum.IN_EXTRA_BED)
state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1 }
state.adultCount = state.adultCount + 1 if (!allowedBedTypes.includes(child.bed)) {
}) child.bed = allowedBedTypes[0]
), }
decreaseAdults: (roomIndex) => })
set( })
produce((state: GuestsRooms) => { }
state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1
state.adultCount = state.adultCount - 1 export function initGuestsRoomsState(initData?: GuestsRoom[]) {
if ( const isBrowser = typeof window !== "undefined"
state.rooms[roomIndex].childrenInAdultsBed > const sessionData = isBrowser
state.rooms[roomIndex].adults ? sessionStorage.getItem(SESSION_STORAGE_KEY)
) { : null
const toUpdateIndex = state.rooms[roomIndex].children.findIndex(
(child) => child.bed == BedTypeEnum.IN_ADULTS_BED const defaultGuestsData: extendedGuestsRoom = {
) adults: 1,
if (toUpdateIndex != -1) { child: [],
state.rooms[roomIndex].children[toUpdateIndex].bed = childrenInAdultsBed: 0,
state.rooms[roomIndex].children[toUpdateIndex].age < 3 }
? BedTypeEnum.IN_CRIB const defaultData: GuestsRoomsState = {
: BedTypeEnum.IN_EXTRA_BED rooms: [defaultGuestsData],
state.rooms[roomIndex].childrenInAdultsBed = adultCount: 1,
state.rooms[roomIndex].adults childCount: 0,
} isValidated: false,
} }
})
), let inputData: GuestsRoomsState = defaultData
increaseChildren: (roomIndex) => if (sessionData) {
set( inputData = JSON.parse(sessionData)
produce((state: GuestsRooms) => { }
state.rooms[roomIndex].children.push({ if (initData) {
age: -1, inputData.rooms = initData.map((room) => {
bed: -1, const childrenInAdultsBed = room.child
? room.child.reduce((acc, child) => {
acc = acc + (child.bed == BedTypeEnum.IN_ADULTS_BED ? 1 : 0)
return acc
}, 0)
: 0
return { ...defaultGuestsData, ...room, childrenInAdultsBed }
}) as extendedGuestsRoom[]
inputData.adultCount = initData.reduce((acc, room) => {
acc = acc + room.adults
return acc
}, 0)
inputData.childCount = initData.reduce((acc, room) => {
acc = acc + room.child?.length
return acc
}, 0)
validateBedTypes(inputData.rooms)
}
return create<GuestsRoomsStoreState>()((set, get) => ({
...inputData,
increaseAdults: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1
state.adultCount = state.adultCount + 1
}) })
state.childCount = state.childCount + 1 ),
}) decreaseAdults: (roomIndex) =>
), set(
decreaseChildren: (roomIndex) => { produce((state: GuestsRoomsState) => {
set( state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1
produce((state: GuestsRooms) => { state.adultCount = state.adultCount - 1
const roomChildren = state.rooms[roomIndex].children if (
if ( state.rooms[roomIndex].childrenInAdultsBed >
roomChildren.length && state.rooms[roomIndex].adults
roomChildren[roomChildren.length - 1].bed == BedTypeEnum.IN_ADULTS_BED ) {
) { const toUpdateIndex = state.rooms[roomIndex].child.findIndex(
(child) => child.bed == BedTypeEnum.IN_ADULTS_BED
)
if (toUpdateIndex != -1) {
state.rooms[roomIndex].child[toUpdateIndex].bed =
state.rooms[roomIndex].child[toUpdateIndex].age < 3
? BedTypeEnum.IN_CRIB
: BedTypeEnum.IN_EXTRA_BED
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].adults
}
}
})
),
increaseChildren: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].child.push({
age: -1,
bed: -1,
})
state.childCount = state.childCount + 1
})
),
decreaseChildren: (roomIndex) => {
set(
produce((state: GuestsRoomsState) => {
const roomChildren = state.rooms[roomIndex].child
if (
roomChildren.length &&
roomChildren[roomChildren.length - 1].bed ==
BedTypeEnum.IN_ADULTS_BED
) {
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed - 1
}
state.rooms[roomIndex].child.pop()
state.childCount = state.childCount - 1
})
)
return get().rooms[roomIndex].child
},
updateChildAge: (age, roomIndex, childIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].child[childIndex].age = age
})
),
updateChildBed: (bed, roomIndex, childIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].child[childIndex].bed = bed
})
),
increaseChildInAdultsBed: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed + 1
})
),
decreaseChildInAdultsBed: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms[roomIndex].childrenInAdultsBed = state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed - 1 state.rooms[roomIndex].childrenInAdultsBed - 1
}
state.rooms[roomIndex].children.pop()
state.childCount = state.childCount - 1
})
)
return get().rooms[roomIndex].children
},
updateChildAge: (age, roomIndex, childIndex) =>
set(
produce((state: GuestsRooms) => {
state.rooms[roomIndex].children[childIndex].age = age
})
),
updateChildBed: (bed, roomIndex, childIndex) =>
set(
produce((state: GuestsRooms) => {
state.rooms[roomIndex].children[childIndex].bed = bed
})
),
increaseChildInAdultsBed: (roomIndex) =>
set(
produce((state: GuestsRooms) => {
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed + 1
})
),
decreaseChildInAdultsBed: (roomIndex) =>
set(
produce((state: GuestsRooms) => {
state.rooms[roomIndex].childrenInAdultsBed =
state.rooms[roomIndex].childrenInAdultsBed - 1
})
),
increaseRoom: () =>
set(
produce((state: GuestsRooms) => {
state.rooms.push({
adults: 1,
children: [],
childrenInAdultsBed: 0,
}) })
}) ),
), increaseRoom: () =>
decreaseRoom: (roomIndex) => set(
set( produce((state: GuestsRoomsState) => {
produce((state: GuestsRooms) => { state.rooms.push({
state.rooms.splice(roomIndex, 1) adults: 1,
}) child: [],
), childrenInAdultsBed: 0,
setIsValidated: (isValidated) => set(() => ({ isValidated })), })
})) })
),
decreaseRoom: (roomIndex) =>
set(
produce((state: GuestsRoomsState) => {
state.rooms.splice(roomIndex, 1)
})
),
setIsValidated: (isValidated) => set(() => ({ isValidated })),
}))
}
export type GuestsRoomsStore = ReturnType<typeof initGuestsRoomsState>
export const GuestsRoomsContext = createContext<GuestsRoomsStore | null>(null)
export const useGuestsRoomsStore = <T>(
selector: (store: GuestsRoomsStoreState) => T
): T => {
const guestsRoomsContextStore = useContext(GuestsRoomsContext)
if (!guestsRoomsContextStore) {
throw new Error(
`guestsRoomsContextStore must be used within GuestsRoomsContextProvider`
)
}
return useStore(guestsRoomsContextStore, selector)
}

View File

@@ -10,7 +10,7 @@ export type Child = {
export type GuestsRoom = { export type GuestsRoom = {
adults: number adults: number
children: Child[] child: Child[]
} }
export interface GuestsRoomsPickerProps { export interface GuestsRoomsPickerProps {

View File

@@ -4,6 +4,8 @@ import { z } from "zod"
import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema"
import { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants" import { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants"
import { GuestsRoom } from "./guestsRoomsPicker"
import type { Locations } from "@/types/trpc/routers/hotel/locations" import type { Locations } from "@/types/trpc/routers/hotel/locations"
export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema> export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema>
@@ -13,22 +15,27 @@ export type BookingWidgetSearchParams = {
hotel?: string hotel?: string
fromDate?: string fromDate?: string
toDate?: string toDate?: string
room?: string room?: GuestsRoom[]
[key: string]: string | string[] | GuestsRoom[] | undefined
} }
export type BookingWidgetType = VariantProps< export type BookingWidgetType = VariantProps<
typeof bookingWidgetVariants typeof bookingWidgetVariants
>["type"] >["type"]
export interface BookingWidgetPageProps {
searchParams?: URLSearchParams
}
export interface BookingWidgetProps { export interface BookingWidgetProps {
type?: BookingWidgetType type?: BookingWidgetType
searchParams?: BookingWidgetSearchParams searchParams?: URLSearchParams
} }
export interface BookingWidgetClientProps { export interface BookingWidgetClientProps {
locations: Locations locations: Locations
type?: BookingWidgetType type?: BookingWidgetType
searchParams?: BookingWidgetSearchParams searchParams?: URLSearchParams
} }
export interface BookingWidgetToggleButtonProps { export interface BookingWidgetToggleButtonProps {

View File

@@ -9,3 +9,36 @@ export function removeTrailingSlash(pathname: string) {
} }
return pathname return pathname
} }
export function getFormattedUrlQueryParams(
searchParams: URLSearchParams,
dataTypes: Record<string, unknown>
) {
const searchParamsObject: Record<string, unknown> = Array.from(
searchParams.entries()
).reduce<Record<string, unknown>>(
(acc, [key, value]) => {
const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.'
keys.reduce((nestedAcc, k, i) => {
if (i === keys.length - 1) {
if (dataTypes[k] == "number") {
;(nestedAcc as Record<string, unknown>)[k] = Number(value)
} else if (dataTypes[k] == "boolean") {
;(nestedAcc as Record<string, unknown>)[k] =
value.toLowerCase() === "true"
} else {
;(nestedAcc as Record<string, unknown>)[k] = value
}
} else {
if (!nestedAcc[k]) {
nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array
}
}
return nestedAcc[k] as Record<string, unknown>
}, acc)
return acc
},
{} as Record<string, unknown>
)
return searchParamsObject
}