sub-task/ SW-695 Prefill Guests data in booking widget
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
33
utils/url.ts
33
utils/url.ts
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user