Merged in fix/refactor-booking-flow-search-params (pull request #2148)

Fix: refactor booking flow search params

* wip: apply codemod and upgrade swc plugin

* wip: design-system to react 19, fix issues from async (search)params

* Prepare new parse function for booking flow search params

* Prepare serialize function for booking flow search params

* Improve handling of comma separated arrays

* Slightly refactor for readability

* Next abstracts URLSearchParams so handle the abstraction instead

* Refactor booking widget to use new search params parsing

* Rename search param functions

* Refactor select-hotel to use new search param parser

* Use new search params parser in select-rate and details

* Fix hotelId type

* Avoid passing down search params into BookingWidget components

* More updates to use new types instead of SearchParams<T>

* Remove types SelectHotelSearchParams and AlternativeSelectHotelSearchParams

* Fix parseBookingWidgetSearchParams return type

* Add error handling to booking search param parsers

* Fix modifyRateIndex handling in details page

* Clean up

* Refactor booking widget search param serializing to util function

* Move start page booking widget search param parsing to page

* Use new search param serializer in HandleErrorCallback

* Delete convertSearchParamsToObj & convertObjToSearchParams


Approved-by: Michael Zetterberg
This commit is contained in:
Anton Gunnarsson
2025-06-02 13:38:01 +00:00
parent 81887c83ff
commit 03468ad824
49 changed files with 1257 additions and 444 deletions

View File

@@ -3,19 +3,21 @@ import { notFound } from "next/navigation"
import { env } from "@/env/server" import { env } from "@/env/server"
import StartPage from "@/components/ContentType/StartPage" import StartPage from "@/components/ContentType/StartPage"
import { parseBookingWidgetSearchParams } from "@/utils/url"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget" import type { NextSearchParams, PageArgs } from "@/types/params"
import type { PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/generateMetadata" export { generateMetadata } from "@/utils/generateMetadata"
export default async function StartPagePage( export default async function StartPagePage(
props: PageArgs<{}, BookingWidgetSearchData> props: PageArgs<{}, NextSearchParams>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") { if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
return notFound() return notFound()
} }
return <StartPage searchParams={searchParams} /> const booking = parseBookingWidgetSearchParams(searchParams)
return <StartPage booking={booking} />
} }

View File

@@ -1,22 +1,23 @@
import { notFound } from "next/navigation"
import { Suspense } from "react" import { Suspense } from "react"
import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer"
import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton"
import { MapContainer } from "@/components/MapContainer" import { MapContainer } from "@/components/MapContainer"
import { convertSearchParamsToObj } from "@/utils/url" import { parseSelectHotelSearchParams } from "@/utils/url"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { AlternativeHotelsSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage( export default async function SelectHotelMapPage(
props: PageArgs<LangParams, AlternativeHotelsSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const booking = const booking = parseSelectHotelSearchParams(searchParams)
convertSearchParamsToObj<AlternativeHotelsSearchParams>(searchParams)
if (!booking) return notFound()
return ( return (
<div className={styles.main}> <div className={styles.main}>

View File

@@ -13,23 +13,19 @@ import { getTracking } from "@/components/HotelReservation/SelectHotel/tracking"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { convertSearchParamsToObj } from "@/utils/url" import { parseSelectHotelSearchParams } from "@/utils/url"
import type { import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
AlternativeHotelsSearchParams,
SelectHotelSearchParams,
} from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { LangParams, PageArgs } from "@/types/params"
export default async function AlternativeHotelsPage( export default async function AlternativeHotelsPage(
props: PageArgs<LangParams, AlternativeHotelsSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const params = await props.params const params = await props.params
const booking = const booking = parseSelectHotelSearchParams(searchParams)
convertSearchParamsToObj<AlternativeHotelsSearchParams>(searchParams)
if (!booking) return notFound()
const searchDetails = await getHotelSearchDetails(booking, true) const searchDetails = await getHotelSearchDetails(booking, true)
@@ -42,10 +38,10 @@ export default async function AlternativeHotelsPage(
bookingCode, bookingCode,
childrenInRoom, childrenInRoom,
city, city,
cityName,
hotel: isAlternativeFor, hotel: isAlternativeFor,
noOfRooms, noOfRooms,
redemption, redemption,
selectHotelParams,
} = searchDetails } = searchDetails
if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) { if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) {
@@ -59,16 +55,18 @@ export default async function AlternativeHotelsPage(
// TODO: This needs to be refactored into its // TODO: This needs to be refactored into its
// own functions // own functions
const hotels = await getHotels( const hotels = await getHotels({
selectHotelParams, fromDate: booking.fromDate,
toDate: booking.toDate,
rooms: booking.rooms,
isAlternativeFor, isAlternativeFor,
bookingCode, bookingCode,
city, city,
!!redemption redemption: !!redemption,
) })
const arrivalDate = new Date(selectHotelParams.fromDate) const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(selectHotelParams.toDate) const departureDate = new Date(booking.toDate)
const isRedemptionAvailability = redemption const isRedemptionAvailability = redemption
? hotels.some( ? hotels.some(
@@ -92,11 +90,11 @@ export default async function AlternativeHotelsPage(
adultsInRoom, adultsInRoom,
childrenInRoom, childrenInRoom,
hotels?.length ?? 0, hotels?.length ?? 0,
selectHotelParams.hotelId, booking.hotelId,
noOfRooms, noOfRooms,
hotels?.[0]?.hotel.address.country, hotels?.[0]?.hotel.address.country,
hotels?.[0]?.hotel.address.city, hotels?.[0]?.hotel.address.city,
selectHotelParams.city, cityName,
bookingCode, bookingCode,
isBookingCodeRateAvailable, isBookingCodeRateAvailable,
redemption, redemption,

View File

@@ -20,15 +20,14 @@ import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDeta
import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert" import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert"
import RoomProvider from "@/providers/Details/RoomProvider" import RoomProvider from "@/providers/Details/RoomProvider"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider" import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
import { convertSearchParamsToObj } from "@/utils/url" import { parseDetailsSearchParams } from "@/utils/url"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function DetailsPage( export default async function DetailsPage(
props: PageArgs<LangParams, SelectRateSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const params = await props.params const params = await props.params
@@ -37,11 +36,12 @@ export default async function DetailsPage(
const selectRoomParams = new URLSearchParams(searchParams) const selectRoomParams = new URLSearchParams(searchParams)
const { errorCode, ...booking } = const booking = parseDetailsSearchParams(searchParams)
convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
if ("modifyRateIndex" in booking) { if (!booking) return notFound()
if (selectRoomParams.has("modifyRateIndex")) {
selectRoomParams.delete("modifyRateIndex") selectRoomParams.delete("modifyRateIndex")
delete booking.modifyRateIndex
} }
if ( if (

View File

@@ -1,24 +1,25 @@
import stringify from "json-stable-stringify-without-jsonify" import stringify from "json-stable-stringify-without-jsonify"
import { notFound } from "next/navigation"
import { Suspense } from "react" import { Suspense } from "react"
import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer"
import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton"
import { MapContainer } from "@/components/MapContainer" import { MapContainer } from "@/components/MapContainer"
import { convertSearchParamsToObj } from "@/utils/url" import { parseSelectHotelSearchParams } from "@/utils/url"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage( export default async function SelectHotelMapPage(
props: PageArgs<LangParams, SelectHotelSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const suspenseKey = stringify(searchParams) const suspenseKey = stringify(searchParams)
const booking = const booking = parseSelectHotelSearchParams(searchParams)
convertSearchParamsToObj<SelectHotelSearchParams>(searchParams)
if (!booking) return notFound()
return ( return (
<div className={styles.main}> <div className={styles.main}>

View File

@@ -12,19 +12,19 @@ import { getHotels } from "@/components/HotelReservation/SelectHotel/helpers"
import { getTracking } from "@/components/HotelReservation/SelectHotel/tracking" import { getTracking } from "@/components/HotelReservation/SelectHotel/tracking"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { convertSearchParamsToObj } from "@/utils/url" import { parseSelectHotelSearchParams } from "@/utils/url"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelPage( export default async function SelectHotelPage(
props: PageArgs<LangParams, SelectHotelSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const params = await props.params const params = await props.params
const booking = const booking = parseSelectHotelSearchParams(searchParams)
convertSearchParamsToObj<SelectHotelSearchParams>(searchParams)
if (!booking) return notFound()
const searchDetails = await getHotelSearchDetails(booking) const searchDetails = await getHotelSearchDetails(booking)
@@ -35,9 +35,9 @@ export default async function SelectHotelPage(
bookingCode, bookingCode,
childrenInRoom, childrenInRoom,
city, city,
cityName,
noOfRooms, noOfRooms,
redemption, redemption,
selectHotelParams,
} = searchDetails } = searchDetails
if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) { if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) {
@@ -49,16 +49,18 @@ export default async function SelectHotelPage(
} }
} }
const hotels = await getHotels( const hotels = await getHotels({
selectHotelParams, fromDate: booking.fromDate,
null, toDate: booking.toDate,
rooms: booking.rooms,
isAlternativeFor: null,
bookingCode, bookingCode,
city, city,
!!redemption redemption: !!redemption,
) })
const arrivalDate = new Date(selectHotelParams.fromDate) const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(selectHotelParams.toDate) const departureDate = new Date(booking.toDate)
const isRedemptionAvailability = redemption const isRedemptionAvailability = redemption
? hotels.some( ? hotels.some(
@@ -82,11 +84,11 @@ export default async function SelectHotelPage(
adultsInRoom, adultsInRoom,
childrenInRoom, childrenInRoom,
hotels?.length ?? 0, hotels?.length ?? 0,
selectHotelParams.hotelId, booking.hotelId,
noOfRooms, noOfRooms,
hotels?.[0]?.hotel.address.country, hotels?.[0]?.hotel.address.country,
hotels?.[0]?.hotel.address.city, hotels?.[0]?.hotel.address.city,
selectHotelParams.city, cityName,
bookingCode, bookingCode,
isBookingCodeRateAvailable, isBookingCodeRateAvailable,
redemption, redemption,

View File

@@ -3,10 +3,9 @@ import { notFound } from "next/navigation"
import { combineRegExps, rateTypeRegex, REDEMPTION } from "@/constants/booking" import { combineRegExps, rateTypeRegex, REDEMPTION } from "@/constants/booking"
import SelectRate from "@/components/HotelReservation/SelectRate" import SelectRate from "@/components/HotelReservation/SelectRate"
import { convertSearchParamsToObj } from "@/utils/url" import { parseSelectRateSearchParams } from "@/utils/url"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
const singleRoomRateTypes = combineRegExps( const singleRoomRateTypes = combineRegExps(
[rateTypeRegex.ARB, rateTypeRegex.VOUCHER], [rateTypeRegex.ARB, rateTypeRegex.VOUCHER],
@@ -14,11 +13,13 @@ const singleRoomRateTypes = combineRegExps(
) )
export default async function SelectRatePage( export default async function SelectRatePage(
props: PageArgs<LangParams & { section: string }, SelectRateSearchParams> props: PageArgs<LangParams & { section: string }, NextSearchParams>
) { ) {
const params = await props.params const params = await props.params
const searchParams = await props.searchParams const searchParams = await props.searchParams
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams) const booking = parseSelectRateSearchParams(searchParams)
if (!booking) return notFound()
const isMultiRoom = booking.rooms.length > 1 const isMultiRoom = booking.rooms.length > 1
const isRedemption = booking.searchType === REDEMPTION const isRedemption = booking.searchType === REDEMPTION

View File

@@ -2,12 +2,14 @@ import { env } from "@/env/server"
import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests" import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests"
import { BookingWidget } from "@/components/BookingWidget" import { BookingWidget } from "@/components/BookingWidget"
import { parseBookingWidgetSearchParams } from "@/utils/url"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget" import type { NextSearchParams, PageArgs } from "@/types/params"
import type { PageArgs } from "@/types/params"
export default async function BookingWidgetDestinationCityPage(props: PageArgs<{}, BookingWidgetSearchData>) { export default async function BookingWidgetDestinationCityPage(
const searchParams = await props.searchParams; props: PageArgs<{}, NextSearchParams>
) {
const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") { if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
return null return null
} }
@@ -20,5 +22,7 @@ export default async function BookingWidgetDestinationCityPage(props: PageArgs<{
city: pageData?.city.name ?? "", city: pageData?.city.name ?? "",
} }
return <BookingWidget bookingWidgetSearchParams={bookingWidgetSearchParams} /> const booking = parseBookingWidgetSearchParams(bookingWidgetSearchParams)
return <BookingWidget booking={booking} />
} }

View File

@@ -3,20 +3,18 @@ import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
import { BookingWidget } from "@/components/BookingWidget" import { BookingWidget } from "@/components/BookingWidget"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import { parseBookingWidgetSearchParams } from "@/utils/url"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget" import type { NextSearchParams, PageArgs } from "@/types/params"
import type { PageArgs } from "@/types/params"
export default async function BookingWidgetHotelPage( export default async function BookingWidgetHotelPage(
props: PageArgs<{}, BookingWidgetSearchData & { subpage?: string }> props: PageArgs<{}, NextSearchParams & { subpage?: string }>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") { if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
return null return null
} }
const { bookingCode, subpage } = searchParams
const hotelPageData = await getHotelPage() const hotelPageData = await getHotelPage()
const hotelData = await getHotel({ const hotelData = await getHotel({
hotelId: hotelPageData?.hotel_page_id || "", hotelId: hotelPageData?.hotel_page_id || "",
@@ -24,6 +22,7 @@ export default async function BookingWidgetHotelPage(
isCardOnlyPayment: false, isCardOnlyPayment: false,
}) })
const subpage = searchParams.subpage
const isMeetingSubpage = const isMeetingSubpage =
subpage && hotelData?.additionalData.meetingRooms.nameInUrl === subpage subpage && hotelData?.additionalData.meetingRooms.nameInUrl === subpage
@@ -32,10 +31,12 @@ export default async function BookingWidgetHotelPage(
} }
const bookingWidgetSearchParams = { const bookingWidgetSearchParams = {
bookingCode: bookingCode ?? "", bookingCode: searchParams.bookingCode ?? "",
hotel: hotelData?.hotel.id ?? "", hotel: hotelData?.hotel.id ?? "",
city: hotelData?.hotel.cityName ?? "", city: hotelData?.hotel.cityName ?? "",
} }
return <BookingWidget bookingWidgetSearchParams={bookingWidgetSearchParams} /> const booking = parseBookingWidgetSearchParams(bookingWidgetSearchParams)
return <BookingWidget booking={booking} />
} }

View File

@@ -1,12 +1,14 @@
import { BookingWidget } from "@/components/BookingWidget" import { BookingWidget } from "@/components/BookingWidget"
import { parseBookingWidgetSearchParams } from "@/utils/url"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function BookingWidgetPage( export default async function BookingWidgetPage(
props: PageArgs<LangParams, BookingWidgetSearchData> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
return <BookingWidget bookingWidgetSearchParams={searchParams} /> const booking = parseBookingWidgetSearchParams(searchParams)
return <BookingWidget booking={booking} />
} }

View File

@@ -1,16 +1,20 @@
import { env } from "@/env/server" import { env } from "@/env/server"
import { BookingWidget } from "@/components/BookingWidget" import { BookingWidget } from "@/components/BookingWidget"
import { parseBookingWidgetSearchParams } from "@/utils/url"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function BookingWidgetPage(props: PageArgs<LangParams, BookingWidgetSearchData>) { export default async function BookingWidgetPage(
const params = await props.params; props: PageArgs<LangParams, NextSearchParams>
const searchParams = await props.searchParams; ) {
const params = await props.params
const searchParams = await props.searchParams
if (!env.isLangLive(params.lang)) { if (!env.isLangLive(params.lang)) {
return null return null
} }
return <BookingWidget bookingWidgetSearchParams={searchParams} /> const booking = parseBookingWidgetSearchParams(searchParams)
return <BookingWidget booking={booking} />
} }

View File

@@ -19,7 +19,6 @@ import useLang from "@/hooks/useLang"
import useStickyPosition from "@/hooks/useStickyPosition" import useStickyPosition from "@/hooks/useStickyPosition"
import { debounce } from "@/utils/debounce" import { debounce } from "@/utils/debounce"
import isValidJson from "@/utils/isValidJson" import isValidJson from "@/utils/isValidJson"
import { convertSearchParamsToObj } from "@/utils/url"
import MobileToggleButton, { import MobileToggleButton, {
MobileToggleButtonSkeleton, MobileToggleButtonSkeleton,
@@ -35,12 +34,11 @@ import type {
BookingCodeSchema, BookingCodeSchema,
BookingWidgetClientProps, BookingWidgetClientProps,
BookingWidgetSchema, BookingWidgetSchema,
BookingWidgetSearchData,
} from "@/types/components/bookingWidget" } from "@/types/components/bookingWidget"
export default function BookingWidgetClient({ export default function BookingWidgetClient({
type, type,
bookingWidgetSearchParams, data,
pageSettingsBookingCodePromise, pageSettingsBookingCodePromise,
}: BookingWidgetClientProps) { }: BookingWidgetClientProps) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
@@ -50,22 +48,19 @@ export default function BookingWidgetClient({
null null
) )
const params = convertSearchParamsToObj<BookingWidgetSearchData>( const shouldFetchAutoComplete = !!data.hotelId || !!data.city
bookingWidgetSearchParams
)
const shouldFetchAutoComplete = !!params.hotelId || !!params.city const { data: destinationsData, isPending } =
trpc.autocomplete.destinations.useQuery(
const { data, isPending } = trpc.autocomplete.destinations.useQuery( {
{ lang,
lang, query: "",
query: "", includeTypes: ["hotels", "cities"],
includeTypes: ["hotels", "cities"], selectedHotelId: data.hotelId ? data.hotelId.toString() : undefined,
selectedHotelId: params.hotelId, selectedCity: data.city,
selectedCity: params.city, },
}, { enabled: shouldFetchAutoComplete }
{ enabled: shouldFetchAutoComplete } )
)
const shouldShowSkeleton = shouldFetchAutoComplete && isPending const shouldShowSkeleton = shouldFetchAutoComplete && isPending
useStickyPosition({ useStickyPosition({
@@ -76,8 +71,8 @@ export default function BookingWidgetClient({
const now = dt() const now = dt()
// if fromDate or toDate is undefined, dt will return value that represents the same as 'now' above. // if fromDate or toDate is undefined, dt will return value that represents the same as 'now' above.
// this is fine as isDateParamValid will catch this and default the values accordingly. // this is fine as isDateParamValid will catch this and default the values accordingly.
let fromDate = dt(params.fromDate) let fromDate = dt(data.fromDate)
let toDate = dt(params.toDate) let toDate = dt(data.toDate)
const isDateParamValid = const isDateParamValid =
fromDate.isValid() && fromDate.isValid() &&
@@ -91,17 +86,18 @@ export default function BookingWidgetClient({
} }
let selectedLocation = let selectedLocation =
data?.currentSelection.hotel ?? data?.currentSelection.city destinationsData?.currentSelection.hotel ??
destinationsData?.currentSelection.city
// if bookingCode is not provided in the search params, // if bookingCode is not provided in the search params,
// we will fetch it from the page settings stored in Contentstack. // we will fetch it from the page settings stored in Contentstack.
const selectedBookingCode = const selectedBookingCode =
params.bookingCode || data.bookingCode ||
(pageSettingsBookingCodePromise !== null (pageSettingsBookingCodePromise !== null
? use(pageSettingsBookingCodePromise) ? use(pageSettingsBookingCodePromise)
: "") : "")
const defaultRoomsData: BookingWidgetSchema["rooms"] = params.rooms?.map( const defaultRoomsData: BookingWidgetSchema["rooms"] = data.rooms?.map(
(room) => ({ (room) => ({
adults: room.adults, adults: room.adults,
childrenInRoom: room.childrenInRoom || [], childrenInRoom: room.childrenInRoom || [],
@@ -112,7 +108,7 @@ export default function BookingWidgetClient({
childrenInRoom: [], childrenInRoom: [],
}, },
] ]
const hotelId = isNaN(+params.hotelId) ? undefined : +params.hotelId const hotelId = data.hotelId ? parseInt(data.hotelId) : undefined
const methods = useForm({ const methods = useForm({
defaultValues: { defaultValues: {
search: selectedLocation?.name ?? "", search: selectedLocation?.name ?? "",
@@ -126,9 +122,9 @@ export default function BookingWidgetClient({
value: selectedBookingCode, value: selectedBookingCode,
remember: false, remember: false,
}, },
redemption: params?.searchType === REDEMPTION, redemption: data.searchType === REDEMPTION,
rooms: defaultRoomsData, rooms: defaultRoomsData,
city: params.city || undefined, city: data.city || undefined,
hotel: hotelId, hotel: hotelId,
}, },
shouldFocusError: false, shouldFocusError: false,

View File

@@ -8,7 +8,7 @@ import { FloatingBookingWidgetClient } from "./FloatingBookingWidgetClient"
import type { BookingWidgetProps } from "@/types/components/bookingWidget" import type { BookingWidgetProps } from "@/types/components/bookingWidget"
export async function FloatingBookingWidget({ export async function FloatingBookingWidget({
bookingWidgetSearchParams, booking,
}: Omit<BookingWidgetProps, "type">) { }: Omit<BookingWidgetProps, "type">) {
const isHidden = await isBookingWidgetHidden() const isHidden = await isBookingWidgetHidden()
@@ -17,13 +17,13 @@ export async function FloatingBookingWidget({
} }
let pageSettingsBookingCodePromise: Promise<string> | null = null let pageSettingsBookingCodePromise: Promise<string> | null = null
if (!bookingWidgetSearchParams.bookingCode) { if (!booking.bookingCode) {
pageSettingsBookingCodePromise = getPageSettingsBookingCode() pageSettingsBookingCodePromise = getPageSettingsBookingCode()
} }
return ( return (
<FloatingBookingWidgetClient <FloatingBookingWidgetClient
bookingWidgetSearchParams={bookingWidgetSearchParams} data={booking}
pageSettingsBookingCodePromise={pageSettingsBookingCodePromise} pageSettingsBookingCodePromise={pageSettingsBookingCodePromise}
/> />
) )

View File

@@ -17,10 +17,7 @@ export async function BookingWidget(props: BookingWidgetProps) {
) )
} }
async function InternalBookingWidget({ async function InternalBookingWidget({ type, booking }: BookingWidgetProps) {
type,
bookingWidgetSearchParams,
}: BookingWidgetProps) {
const isHidden = await isBookingWidgetHidden() const isHidden = await isBookingWidgetHidden()
if (isHidden) { if (isHidden) {
@@ -28,14 +25,14 @@ async function InternalBookingWidget({
} }
let pageSettingsBookingCodePromise: Promise<string> | null = null let pageSettingsBookingCodePromise: Promise<string> | null = null
if (!bookingWidgetSearchParams.bookingCode) { if (!booking.bookingCode) {
pageSettingsBookingCodePromise = getPageSettingsBookingCode() pageSettingsBookingCodePromise = getPageSettingsBookingCode()
} }
return ( return (
<BookingWidgetClient <BookingWidgetClient
type={type} type={type}
bookingWidgetSearchParams={bookingWidgetSearchParams} data={booking}
pageSettingsBookingCodePromise={pageSettingsBookingCodePromise} pageSettingsBookingCodePromise={pageSettingsBookingCodePromise}
/> />
) )

View File

@@ -9,12 +9,13 @@ import TrackingSDK from "@/components/TrackingSDK"
import styles from "./startPage.module.css" import styles from "./startPage.module.css"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget"
import { BlocksEnums } from "@/types/enums/blocks" import { BlocksEnums } from "@/types/enums/blocks"
export default async function StartPage({ export default async function StartPage({
searchParams, booking,
}: { }: {
searchParams: { [key: string]: string } booking: BookingWidgetSearchData
}) { }) {
const content = await getStartPage() const content = await getStartPage()
if (!content) { if (!content) {
@@ -30,7 +31,7 @@ export default async function StartPage({
<Title color="white" textAlign="center"> <Title color="white" textAlign="center">
{header.heading} {header.heading}
</Title> </Title>
<FloatingBookingWidget bookingWidgetSearchParams={searchParams} /> <FloatingBookingWidget booking={booking} />
</div> </div>
{header.hero_image ? ( {header.hero_image ? (
<Image <Image

View File

@@ -9,7 +9,7 @@ import { REDEMPTION } from "@/constants/booking"
import { selectHotel, selectRate } from "@/constants/routes/hotelReservation" import { selectHotel, selectRate } from "@/constants/routes/hotelReservation"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { convertObjToSearchParams } from "@/utils/url" import { serializeBookingSearchParams } from "@/utils/url"
import FormContent, { BookingWidgetFormContentSkeleton } from "./FormContent" import FormContent, { BookingWidgetFormContentSkeleton } from "./FormContent"
import { bookingWidgetVariants } from "./variants" import { bookingWidgetVariants } from "./variants"
@@ -38,7 +38,7 @@ export default function Form({ type, onClose }: BookingWidgetFormProps) {
function onSubmit(data: BookingWidgetSchema) { function onSubmit(data: BookingWidgetSchema) {
const bookingFlowPage = data.hotel ? selectRate(lang) : selectHotel(lang) const bookingFlowPage = data.hotel ? selectRate(lang) : selectHotel(lang)
const bookingWidgetParams = convertObjToSearchParams({ const bookingWidgetParams = serializeBookingSearchParams({
rooms: data.rooms, rooms: data.rooms,
...data.date, ...data.date,
...(data.city ? { city: data.city } : {}), ...(data.city ? { city: data.city } : {}),

View File

@@ -9,7 +9,7 @@ import { detailsStorageName } from "@/stores/enter-details"
import LoadingSpinner from "@/components/LoadingSpinner" import LoadingSpinner from "@/components/LoadingSpinner"
import { trackPaymentEvent } from "@/utils/tracking" import { trackPaymentEvent } from "@/utils/tracking"
import { trackEvent } from "@/utils/tracking/base" import { trackEvent } from "@/utils/tracking/base"
import { convertObjToSearchParams } from "@/utils/url" import { serializeBookingSearchParams } from "@/utils/url"
import { clearGlaSessionStorage, readGlaFromSessionStorage } from "./helpers" import { clearGlaSessionStorage, readGlaFromSessionStorage } from "./helpers"
@@ -33,9 +33,11 @@ export default function HandleErrorCallback({
if (bookingData) { if (bookingData) {
const detailsStorage: PersistedState = JSON.parse(bookingData) const detailsStorage: PersistedState = JSON.parse(bookingData)
const searchParams = convertObjToSearchParams( const searchParams = serializeBookingSearchParams(
detailsStorage.booking, detailsStorage.booking,
searchObject {
initialSearchParams: searchObject,
}
) )
const glaSessionData = readGlaFromSessionStorage() const glaSessionData = readGlaFromSessionStorage()

View File

@@ -7,14 +7,13 @@ import { useSearchHistory } from "@/hooks/useSearchHistory"
import { getTracking } from "./tracking" import { getTracking } from "./tracking"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { DetailsBooking } from "@/types/components/hotelReservation/enterDetails/details"
import type { Hotel } from "@/types/hotel" import type { Hotel } from "@/types/hotel"
import type { Room } from "@/types/providers/details/room" import type { Room } from "@/types/providers/details/room"
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
import type { SelectHotelParams } from "@/utils/url"
interface TrackingWrapperProps { interface TrackingWrapperProps {
booking: SelectHotelParams<SelectRateSearchParams> booking: DetailsBooking
hotel: Hotel hotel: Hotel
rooms: Room[] rooms: Room[]
isMember: boolean isMember: boolean

View File

@@ -7,8 +7,8 @@ import { getSpecialRoomType } from "@/utils/specialRoomType"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast" import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast"
import type { DetailsBooking } from "@/types/components/hotelReservation/enterDetails/details"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { import {
TrackingChannelEnum, TrackingChannelEnum,
type TrackingSDKAncillaries, type TrackingSDKAncillaries,
@@ -25,10 +25,9 @@ import type {
Product, Product,
} from "@/types/trpc/routers/hotel/roomAvailability" } from "@/types/trpc/routers/hotel/roomAvailability"
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
import type { SelectHotelParams } from "@/utils/url"
export function getTracking( export function getTracking(
booking: SelectHotelParams<SelectRateSearchParams>, booking: DetailsBooking,
hotel: Hotel, hotel: Hotel,
rooms: Room[], rooms: Room[],
isMember: boolean, isMember: boolean,

View File

@@ -37,23 +37,25 @@ export async function SelectHotelMapContainer({
bookingCode, bookingCode,
childrenInRoom, childrenInRoom,
city, city,
cityName,
hotel: isAlternativeFor, hotel: isAlternativeFor,
noOfRooms, noOfRooms,
redemption, redemption,
selectHotelParams,
} = searchDetails } = searchDetails
if (!city) { if (!city) {
return notFound() return notFound()
} }
const hotels = await getHotels( const hotels = await getHotels({
selectHotelParams, fromDate: booking.fromDate,
toDate: booking.toDate,
rooms: booking.rooms,
isAlternativeFor, isAlternativeFor,
bookingCode, bookingCode,
city, city,
!!redemption redemption: !!redemption,
) })
const hotelPins = getHotelPins(hotels) const hotelPins = getHotelPins(hotels)
const filterList = getFiltersFromHotels(hotels) const filterList = getFiltersFromHotels(hotels)
@@ -62,8 +64,8 @@ export async function SelectHotelMapContainer({
hotel: { address: hotels?.[0]?.hotel?.address.streetAddress }, hotel: { address: hotels?.[0]?.hotel?.address.streetAddress },
}) })
const arrivalDate = new Date(selectHotelParams.fromDate) const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(selectHotelParams.toDate) const departureDate = new Date(booking.toDate)
const isRedemptionAvailability = redemption const isRedemptionAvailability = redemption
? hotels.some( ? hotels.some(
(hotel) => hotel.availability.productType?.redemptions?.length (hotel) => hotel.availability.productType?.redemptions?.length
@@ -83,11 +85,11 @@ export async function SelectHotelMapContainer({
adultsInRoom, adultsInRoom,
childrenInRoom, childrenInRoom,
hotels.length, hotels.length,
selectHotelParams.hotelId, booking.hotelId,
noOfRooms, noOfRooms,
hotels?.[0]?.hotel.address.country, hotels?.[0]?.hotel.address.country,
hotels?.[0]?.hotel.address.city, hotels?.[0]?.hotel.address.city,
selectHotelParams.city, cityName,
bookingCode, bookingCode,
isBookingCodeRateAvailable, isBookingCodeRateAvailable,
redemption, redemption,

View File

@@ -18,7 +18,7 @@ export function getTracking(
adultsInRoom: number[], adultsInRoom: number[],
childrenInRoom: ChildrenInRoom, childrenInRoom: ChildrenInRoom,
hotelsResult: number, hotelsResult: number,
hotelId: string, hotelId: string | undefined,
noOfRooms: number, noOfRooms: number,
country: string | undefined, country: string | undefined,
hotelCity: string | undefined, hotelCity: string | undefined,

View File

@@ -14,17 +14,13 @@ import type {
HotelFilter, HotelFilter,
} from "@/types/components/hotelReservation/selectHotel/hotelFilters" } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
AlternativeHotelsSearchParams,
SelectHotelSearchParams,
} from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import type { AdditionalData, Hotel } from "@/types/hotel" import type { AdditionalData, Hotel } from "@/types/hotel"
import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability" import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability"
import type { import type {
HotelLocation, HotelLocation,
Location, Location,
} from "@/types/trpc/routers/hotel/locations" } from "@/types/trpc/routers/hotel/locations"
import type { SelectHotelParams } from "@/utils/url"
interface AvailabilityResponse { interface AvailabilityResponse {
availability: HotelsAvailabilityItem[] availability: HotelsAvailabilityItem[]
@@ -162,19 +158,32 @@ function sortAndFilterHotelsByAvailability(
].flat() ].flat()
} }
export async function getHotels( type GetHotelsInput = {
booking: SelectHotelParams< fromDate: string
SelectHotelSearchParams | AlternativeHotelsSearchParams toDate: string
>, rooms: {
isAlternativeFor: HotelLocation | null, adults: number
bookingCode: string | undefined, childrenInRoom?: Child[]
city: Location, }[]
isAlternativeFor: HotelLocation | null
bookingCode: string | undefined
city: Location
redemption: boolean redemption: boolean
) { }
export async function getHotels({
rooms,
fromDate,
toDate,
isAlternativeFor,
bookingCode,
city,
redemption,
}: GetHotelsInput) {
let availableHotelsResponse: SettledResult = [] let availableHotelsResponse: SettledResult = []
if (isAlternativeFor) { if (isAlternativeFor) {
availableHotelsResponse = await Promise.allSettled( availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(async (room) => { rooms.map(async (room) => {
return fetchAlternativeHotels(isAlternativeFor.id, { return fetchAlternativeHotels(isAlternativeFor.id, {
adults: room.adults, adults: room.adults,
bookingCode, bookingCode,
@@ -182,14 +191,14 @@ export async function getHotels(
? generateChildrenString(room.childrenInRoom) ? generateChildrenString(room.childrenInRoom)
: undefined, : undefined,
redemption, redemption,
roomStayEndDate: booking.toDate, roomStayEndDate: toDate,
roomStayStartDate: booking.fromDate, roomStayStartDate: fromDate,
}) })
}) })
) )
} else if (bookingCode) { } else if (bookingCode) {
availableHotelsResponse = await Promise.allSettled( availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(async (room) => { rooms.map(async (room) => {
return fetchBookingCodeAvailableHotels({ return fetchBookingCodeAvailableHotels({
adults: room.adults, adults: room.adults,
bookingCode, bookingCode,
@@ -197,14 +206,14 @@ export async function getHotels(
? generateChildrenString(room.childrenInRoom) ? generateChildrenString(room.childrenInRoom)
: undefined, : undefined,
cityId: city.id, cityId: city.id,
roomStayStartDate: booking.fromDate, roomStayStartDate: fromDate,
roomStayEndDate: booking.toDate, roomStayEndDate: toDate,
}) })
}) })
) )
} else { } else {
availableHotelsResponse = await Promise.allSettled( availableHotelsResponse = await Promise.allSettled(
booking.rooms.map( rooms.map(
async (room) => async (room) =>
await fetchAvailableHotels({ await fetchAvailableHotels({
adults: room.adults, adults: room.adults,
@@ -213,8 +222,8 @@ export async function getHotels(
: undefined, : undefined,
cityId: city.id, cityId: city.id,
redemption, redemption,
roomStayEndDate: booking.toDate, roomStayEndDate: toDate,
roomStayStartDate: booking.fromDate, roomStayStartDate: fromDate,
}) })
) )
) )

View File

@@ -17,7 +17,7 @@ export function getTracking(
adultsInRoom: number[], adultsInRoom: number[],
childrenInRoom: ChildrenInRoom, childrenInRoom: ChildrenInRoom,
hotelsResult: number, hotelsResult: number,
hotelId: string, hotelId: string | undefined,
noOfRooms: number, noOfRooms: number,
country: string | undefined, country: string | undefined,
hotelCity: string | undefined, hotelCity: string | undefined,

View File

@@ -72,7 +72,7 @@ export default function Summary({
) )
const showDiscounted = containsBookingCodeRate || isMember const showDiscounted = containsBookingCodeRate || isMember
const priceDetailsRooms = mapToPrice(rateSummary, booking, isMember) const priceDetailsRooms = mapToPrice(rateSummary, booking.rooms, isMember)
return ( return (
<section className={styles.summary}> <section className={styles.summary}>

View File

@@ -1,12 +1,12 @@
import type { import type {
Rate, Rate,
SelectRateSearchParams, Room as SelectRateRoom,
} from "@/types/components/hotelReservation/selectRate/selectRate" } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Room } from "@/components/HotelReservation/PriceDetailsModal/PriceDetailsTable" import type { Room } from "@/components/HotelReservation/PriceDetailsModal/PriceDetailsTable"
export function mapToPrice( export function mapToPrice(
rooms: (Rate | null)[], rooms: (Rate | null)[],
booking: SelectRateSearchParams, bookingRooms: SelectRateRoom[],
isUserLoggedIn: boolean isUserLoggedIn: boolean
) { ) {
return rooms return rooms
@@ -43,7 +43,7 @@ export function mapToPrice(
} }
} }
const bookingRoom = booking.rooms[idx] const bookingRoom = bookingRooms[idx]
return { return {
adults: bookingRoom.adults, adults: bookingRoom.adults,
bedType: undefined, bedType: undefined,

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useSearchParams } from "next/navigation" import { notFound, useSearchParams } from "next/navigation"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client" import { trpc } from "@/lib/trpc/client"
@@ -9,7 +9,7 @@ import { selectRateRoomsAvailabilityInputSchema } from "@/server/routers/hotels/
import Alert from "@/components/TempDesignSystem/Alert" import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import RatesProvider from "@/providers/RatesProvider" import RatesProvider from "@/providers/RatesProvider"
import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url" import { parseSelectRateSearchParams, searchParamsToRecord } from "@/utils/url"
import RateSummary from "./RateSummary" import RateSummary from "./RateSummary"
import Rooms from "./Rooms" import Rooms from "./Rooms"
@@ -18,7 +18,6 @@ import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
import styles from "./index.module.css" import styles from "./index.module.css"
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer" import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
export function RoomsContainer({ export function RoomsContainer({
@@ -30,10 +29,12 @@ export function RoomsContainer({
const intl = useIntl() const intl = useIntl()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const booking = convertSearchParamsToObj<SelectRateSearchParams>( const booking = parseSelectRateSearchParams(
searchParamsToRecord(searchParams) searchParamsToRecord(searchParams)
) )
if (!booking) return notFound()
const bookingInput = selectRateRoomsAvailabilityInputSchema.safeParse({ const bookingInput = selectRateRoomsAvailabilityInputSchema.safeParse({
booking, booking,
lang, lang,

View File

@@ -7,12 +7,11 @@ import { REDEMPTION } from "@/constants/booking"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url" import { parseSelectRateSearchParams, searchParamsToRecord } from "@/utils/url"
import { getValidDates } from "../getValidDates" import { getValidDates } from "../getValidDates"
import { getTracking } from "./tracking" import { getTracking } from "./tracking"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails" import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
export default function Tracking({ export default function Tracking({
@@ -34,16 +33,13 @@ export default function Tracking({
}) { }) {
const lang = useLang() const lang = useLang()
const params = useSearchParams() const params = useSearchParams()
const selectRateParams = convertSearchParamsToObj<SelectRateSearchParams>( const booking = parseSelectRateSearchParams(searchParamsToRecord(params))
searchParamsToRecord(params)
)
const { fromDate, toDate } = getValidDates( if (!booking) return null
selectRateParams.fromDate,
selectRateParams.toDate
)
const { rooms, searchType, bookingCode, city: paramCity } = selectRateParams const { fromDate, toDate } = getValidDates(booking.fromDate, booking.toDate)
const { rooms, searchType, bookingCode, city: paramCity } = booking
const arrivalDate = fromDate.toDate() const arrivalDate = fromDate.toDate()
const departureDate = toDate.toDate() const departureDate = toDate.toDate()

View File

@@ -12,16 +12,15 @@ import FnFNotAllowedAlert from "../FnFNotAllowedAlert/FnFNotAllowedAlert"
import AvailabilityError from "./AvailabilityError" import AvailabilityError from "./AvailabilityError"
import Tracking from "./Tracking" import Tracking from "./Tracking"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { SelectRateBooking } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
import type { SelectHotelParams } from "@/utils/url"
export default async function SelectRatePage({ export default async function SelectRatePage({
lang, lang,
booking, booking,
}: { }: {
lang: Lang lang: Lang
booking: SelectHotelParams<SelectRateSearchParams> booking: SelectRateBooking
}) { }) {
const searchDetails = await getHotelSearchDetails(booking) const searchDetails = await getHotelSearchDetails(booking)
if (!searchDetails?.hotel) { if (!searchDetails?.hotel) {

View File

@@ -9,8 +9,7 @@ import BedTwinIcon from "@scandic-hotels/design-system/Icons/BedTwinIcon"
import BedWallExtraIcon from "@scandic-hotels/design-system/Icons/BedWallExtraIcon" import BedWallExtraIcon from "@scandic-hotels/design-system/Icons/BedWallExtraIcon"
import type { IconProps } from "@scandic-hotels/design-system/Icons" import type { IconProps } from "@scandic-hotels/design-system/Icons"
import type { JSX } from "react"
import type { JSX } from "react";
export enum BookingStatusEnum { export enum BookingStatusEnum {
BookingCompleted = "BookingCompleted", BookingCompleted = "BookingCompleted",
@@ -39,6 +38,7 @@ export enum ChildBedTypeEnum {
export const FamilyAndFriendsCodes = ["D000029555", "D000029271", "D000029195"] export const FamilyAndFriendsCodes = ["D000029555", "D000029271", "D000029195"]
export const REDEMPTION = "redemption" export const REDEMPTION = "redemption"
export const bookingSearchTypes = [REDEMPTION] as const
export const SEARCHTYPE = "searchtype" export const SEARCHTYPE = "searchtype"
export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError" export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError"

View File

@@ -9,7 +9,7 @@ import { detailsStorageName } from "."
import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details" import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
import type { Price } from "@/types/components/hotelReservation/price" import type { Price } from "@/types/components/hotelReservation/price"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { SelectRateBooking } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency" import { CurrencyEnum } from "@/types/enums/currency"
import type { Package } from "@/types/requests/packages" import type { Package } from "@/types/requests/packages"
import type { PersistedState, RoomState } from "@/types/stores/enter-details" import type { PersistedState, RoomState } from "@/types/stores/enter-details"
@@ -28,8 +28,8 @@ export function extractGuestFromUser(user: NonNullable<SafeUser>) {
} }
export function checkIsSameBooking( export function checkIsSameBooking(
prev: SelectRateSearchParams & { errorCode?: string }, prev: SelectRateBooking & { errorCode?: string },
next: SelectRateSearchParams & { errorCode?: string } next: SelectRateBooking & { errorCode?: string }
) { ) {
const { rooms: prevRooms, errorCode: prevErrorCode, ...prevBooking } = prev const { rooms: prevRooms, errorCode: prevErrorCode, ...prevBooking } = prev

View File

@@ -45,10 +45,14 @@ export function findProduct(
} }
export function findProductInRoom( export function findProductInRoom(
rateCode: string, rateCode: string | undefined,
room: RoomConfiguration, room: RoomConfiguration,
counterRateCode = "" counterRateCode = ""
) { ) {
if (!rateCode) {
return null
}
if (room.campaign.length) { if (room.campaign.length) {
const campaignProduct = room.campaign.find((product) => const campaignProduct = room.campaign.find((product) =>
findProduct(rateCode, product, counterRateCode) findProduct(rateCode, product, counterRateCode)
@@ -84,14 +88,19 @@ export function findProductInRoom(
} }
export function findSelectedRate( export function findSelectedRate(
rateCode: string, rateCode: string | undefined,
counterRateCode: string, counterRateCode: string | undefined,
roomTypeCode: string, roomTypeCode: string | undefined,
rooms: RoomConfiguration[] | AvailabilityError rooms: RoomConfiguration[] | AvailabilityError
) { ) {
if (!Array.isArray(rooms)) { if (!Array.isArray(rooms)) {
return null return null
} }
if (!rateCode) {
return null
}
return rooms.find((room) => { return rooms.find((room) => {
if (room.roomTypeCode !== roomTypeCode) { if (room.roomTypeCode !== roomTypeCode) {
return false return false

View File

@@ -2,14 +2,12 @@
import { create } from "zustand" import { create } from "zustand"
import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url" import { parseSelectRateSearchParams, searchParamsToRecord } from "@/utils/url"
import { checkIsSameBooking } from "./enter-details/helpers" import { checkIsSameBooking } from "./enter-details/helpers"
import type { ReadonlyURLSearchParams } from "next/navigation" import type { ReadonlyURLSearchParams } from "next/navigation"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
interface TrackingStoreState { interface TrackingStoreState {
initialStartTime: number initialStartTime: number
setInitialPageLoadTime: (time: number) => void setInitialPageLoadTime: (time: number) => void
@@ -81,14 +79,15 @@ const useTrackingStore = create<TrackingStoreState>((set, get) => ({
if (!currentPath?.match(/^\/(da|de|en|fi|no|sv)\/(hotelreservation)/)) if (!currentPath?.match(/^\/(da|de|en|fi|no|sv)\/(hotelreservation)/))
return false return false
const previousParamsObject = const previousParamsObject = parseSelectRateSearchParams(
convertSearchParamsToObj<SelectRateSearchParams>( searchParamsToRecord(previousParams)
searchParamsToRecord(previousParams) )
) const currentParamsObject = parseSelectRateSearchParams(
const currentParamsObject = searchParamsToRecord(currentParams)
convertSearchParamsToObj<SelectRateSearchParams>( )
searchParamsToRecord(currentParams)
) if (!previousParamsObject && !currentParamsObject) return false
if (!previousParamsObject || !currentParamsObject) return true
const isSameBooking = checkIsSameBooking( const isSameBooking = checkIsSameBooking(
previousParamsObject, previousParamsObject,

View File

@@ -1,12 +1,12 @@
import type { VariantProps } from "class-variance-authority" import type { VariantProps } from "class-variance-authority"
import type { z } from "zod" import type { z } from "zod"
import type { SearchParams } from "@/types/params"
import type { import type {
bookingCodeSchema, bookingCodeSchema,
bookingWidgetSchema, bookingWidgetSchema,
} from "@/components/Forms/BookingWidget/schema" } from "@/components/Forms/BookingWidget/schema"
import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants" import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants"
import type { BookingSearchType } from "../hotelReservation/booking"
import type { GuestsRoom } from "./guestsRoomsPicker" import type { GuestsRoom } from "./guestsRoomsPicker"
export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema> export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema>
@@ -14,12 +14,12 @@ export type BookingCodeSchema = z.output<typeof bookingCodeSchema>
export type BookingWidgetSearchData = { export type BookingWidgetSearchData = {
city?: string city?: string
hotel?: string hotelId?: string
fromDate?: string fromDate?: string
toDate?: string toDate?: string
rooms?: GuestsRoom[] rooms?: GuestsRoom[]
bookingCode?: string bookingCode?: string
searchType?: "redemption" searchType?: BookingSearchType
} }
export type BookingWidgetType = VariantProps< export type BookingWidgetType = VariantProps<
@@ -28,16 +28,12 @@ export type BookingWidgetType = VariantProps<
export interface BookingWidgetProps { export interface BookingWidgetProps {
type?: BookingWidgetType type?: BookingWidgetType
bookingWidgetSearchParams: Awaited< booking: BookingWidgetSearchData
SearchParams<BookingWidgetSearchData>["searchParams"]
>
} }
export interface BookingWidgetClientProps { export interface BookingWidgetClientProps {
type?: BookingWidgetType type?: BookingWidgetType
bookingWidgetSearchParams: Awaited< data: BookingWidgetSearchData
SearchParams<BookingWidgetSearchData>["searchParams"]
>
pageSettingsBookingCodePromise: Promise<string> | null pageSettingsBookingCodePromise: Promise<string> | null
} }

View File

@@ -0,0 +1,3 @@
import type { bookingSearchTypes } from "@/constants/booking"
export type BookingSearchType = (typeof bookingSearchTypes)[number]

View File

@@ -1,5 +1,6 @@
import type { z } from "zod" import type { z } from "zod"
import type { PackageEnum } from "@/types/requests/packages"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability" import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
import type { getMultiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema" import type { getMultiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema"
@@ -8,7 +9,9 @@ import type {
signedInDetailsSchema, signedInDetailsSchema,
} from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema" } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
import type { productTypePointsSchema } from "@/server/routers/hotels/schemas/productTypePrice" import type { productTypePointsSchema } from "@/server/routers/hotels/schemas/productTypePrice"
import type { BookingSearchType } from "../booking"
import type { Price } from "../price" import type { Price } from "../price"
import type { Child } from "../selectRate/selectRate"
export type DetailsSchema = z.output<typeof guestDetailsSchema> export type DetailsSchema = z.output<typeof guestDetailsSchema>
export type MultiroomDetailsSchema = z.output< export type MultiroomDetailsSchema = z.output<
@@ -31,3 +34,21 @@ export type JoinScandicFriendsCardProps = {
} }
export type RoomRate = Product export type RoomRate = Product
export type DetailsBooking = {
hotelId: string
fromDate: string
toDate: string
city?: string
bookingCode?: string
searchType?: BookingSearchType
rooms: {
adults: number
rateCode: string
roomTypeCode: string
bookingCode?: string
childrenInRoom?: Child[]
counterRateCode?: string
packages?: PackageEnum[]
}[]
}

View File

@@ -5,12 +5,8 @@ import type { Amenities } from "@/types/hotel"
import type { ProductTypeCheque } from "@/types/trpc/routers/hotel/availability" import type { ProductTypeCheque } from "@/types/trpc/routers/hotel/availability"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
import type { imageSchema } from "@/server/routers/hotels/schemas/image" import type { imageSchema } from "@/server/routers/hotels/schemas/image"
import type { SelectHotelParams } from "@/utils/url"
import type { CategorizedFilters } from "./hotelFilters" import type { CategorizedFilters } from "./hotelFilters"
import type { import type { SelectHotelBooking } from "./selectHotel"
AlternativeHotelsSearchParams,
SelectHotelSearchParams,
} from "./selectHotelSearchParams"
export interface HotelListingProps { export interface HotelListingProps {
hotels: HotelResponse[] hotels: HotelResponse[]
@@ -76,8 +72,6 @@ export interface HotelCardDialogListingProps {
} }
export type SelectHotelMapContainerProps = { export type SelectHotelMapContainerProps = {
booking: SelectHotelParams< booking: SelectHotelBooking
SelectHotelSearchParams | AlternativeHotelsSearchParams
>
isAlternativeHotels?: boolean isAlternativeHotels?: boolean
} }

View File

@@ -1,8 +1,7 @@
import type { Hotel } from "@/types/hotel" import type { Hotel } from "@/types/hotel"
import type { Lang } from "@/constants/languages" import type { BookingSearchType } from "../booking"
import type { SelectHotelParams } from "@/utils/url" import type { Child } from "../selectRate/selectRate"
import type { SidePeekEnum } from "../sidePeek" import type { SidePeekEnum } from "../sidePeek"
import type { AlternativeHotelsSearchParams } from "./selectHotelSearchParams"
export enum AvailabilityEnum { export enum AvailabilityEnum {
Available = "Available", Available = "Available",
@@ -20,8 +19,15 @@ export interface ContactProps {
hotel: Hotel hotel: Hotel
} }
export interface SelectHotelProps { export type SelectHotelBooking = {
booking: SelectHotelParams<AlternativeHotelsSearchParams> hotelId?: string
lang: Lang city?: string
isAlternativeHotels?: boolean fromDate: string
toDate: string
rooms: {
adults: number
childrenInRoom?: Child[]
}[]
bookingCode?: string
searchType?: BookingSearchType
} }

View File

@@ -1,19 +0,0 @@
import type { Room } from "../selectRate/selectRate"
export interface SelectHotelSearchParams {
city: string
fromDate: string
toDate: string
rooms: Pick<Room, "adults" | "childrenInRoom">[]
bookingCode: string
searchType?: "redemption"
}
export interface AlternativeHotelsSearchParams {
hotel: string
fromDate: string
toDate: string
rooms: Pick<Room, "adults" | "childrenInRoom">[]
bookingCode: string
searchType?: "redemption"
}

View File

@@ -5,6 +5,7 @@ import type {
RoomConfiguration, RoomConfiguration,
} from "@/types/trpc/routers/hotel/roomAvailability" } from "@/types/trpc/routers/hotel/roomAvailability"
import type { ChildBedMapEnum } from "../../bookingWidget/enums" import type { ChildBedMapEnum } from "../../bookingWidget/enums"
import type { BookingSearchType } from "../booking"
export interface Child { export interface Child {
bed: ChildBedMapEnum bed: ChildBedMapEnum
@@ -15,21 +16,20 @@ export interface Room {
adults: number adults: number
bookingCode?: string bookingCode?: string
childrenInRoom?: Child[] childrenInRoom?: Child[]
counterRateCode: string counterRateCode?: string
packages?: PackageEnum[] packages?: PackageEnum[]
rateCode: string rateCode?: string
roomTypeCode: string roomTypeCode?: string
} }
export interface SelectRateSearchParams { export type SelectRateBooking = {
bookingCode?: string bookingCode?: string
city?: string city?: string
errorCode?: string
fromDate: string fromDate: string
hotelId: string hotelId: string
rooms: Room[] rooms: Room[]
searchType?: BookingSearchType
toDate: string toDate: string
searchType?: "redemption"
} }
export type Rate = { export type Rate = {

View File

@@ -1,9 +1,13 @@
import type { CurrencyEnum } from "@/types/enums/currency" import type { CurrencyEnum } from "@/types/enums/currency"
import type { Packages } from "@/types/requests/packages" import type { Packages } from "@/types/requests/packages"
import type { RoomState } from "@/types/stores/enter-details" import type { RoomState } from "@/types/stores/enter-details"
import type { RoomPrice, RoomRate } from "./enterDetails/details" import type {
DetailsBooking,
RoomPrice,
RoomRate,
} from "./enterDetails/details"
import type { Price } from "./price" import type { Price } from "./price"
import type { Child, SelectRateSearchParams } from "./selectRate/selectRate" import type { Child, SelectRateBooking } from "./selectRate/selectRate"
export type RoomsData = { export type RoomsData = {
rateDetails: string[] | undefined rateDetails: string[] | undefined
@@ -20,7 +24,7 @@ export interface SummaryProps {
} }
export interface EnterDetailsSummaryProps { export interface EnterDetailsSummaryProps {
booking: SelectRateSearchParams booking: DetailsBooking
isMember: boolean isMember: boolean
totalPrice: Price totalPrice: Price
vat: number vat: number
@@ -30,7 +34,7 @@ export interface EnterDetailsSummaryProps {
} }
export interface SelectRateSummaryProps { export interface SelectRateSummaryProps {
booking: SelectRateSearchParams booking: SelectRateBooking
isMember: boolean isMember: boolean
totalPrice: Price totalPrice: Price
vat: number vat: number

View File

@@ -1,6 +1,8 @@
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
import type { PageContentTypeEnum } from "./requests/contentType" import type { PageContentTypeEnum } from "./requests/contentType"
export type NextSearchParams = { [key: string]: string | string[] | undefined }
export type SearchParams<S = {}> = { export type SearchParams<S = {}> = {
searchParams: Promise<S & { [key: string]: string }> searchParams: Promise<S & { [key: string]: string }>
} }

View File

@@ -1,10 +1,10 @@
import type { Room } from "@/types/providers/details/room" import type { Room } from "@/types/providers/details/room"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
import type { BreakfastPackages } from "../components/hotelReservation/breakfast" import type { BreakfastPackages } from "../components/hotelReservation/breakfast"
import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate" import type { DetailsBooking } from "../components/hotelReservation/enterDetails/details"
export interface DetailsProviderProps extends React.PropsWithChildren { export interface DetailsProviderProps extends React.PropsWithChildren {
booking: SelectRateSearchParams booking: DetailsBooking
breakfastPackages: BreakfastPackages breakfastPackages: BreakfastPackages
rooms: Room[] rooms: Room[]
searchParamsStr: string searchParamsStr: string

View File

@@ -1,10 +1,10 @@
import type { Room } from "@/types/hotel" import type { Room } from "@/types/hotel"
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate" import type { SelectRateBooking } from "../components/hotelReservation/selectRate/selectRate"
import type { AvailabilityError } from "../stores/rates" import type { AvailabilityError } from "../stores/rates"
export interface RatesProviderProps extends React.PropsWithChildren { export interface RatesProviderProps extends React.PropsWithChildren {
booking: SelectRateSearchParams booking: SelectRateBooking
hotelType: string | undefined hotelType: string | undefined
roomCategories: Room[] roomCategories: Room[]
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined

View File

@@ -7,19 +7,17 @@ import type {
BedTypeSelection, BedTypeSelection,
} from "@/types/components/hotelReservation/enterDetails/bedType" } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { import type {
DetailsBooking,
DetailsSchema, DetailsSchema,
MultiroomDetailsSchema, MultiroomDetailsSchema,
RoomPrice, RoomPrice,
RoomRate, RoomRate,
SignedInDetailsSchema, SignedInDetailsSchema,
} from "@/types/components/hotelReservation/enterDetails/details" } from "@/types/components/hotelReservation/enterDetails/details"
import type { CurrencyEnum } from "@/types/enums/currency" import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { StepEnum } from "@/types/enums/step" import type { StepEnum } from "@/types/enums/step"
import type { Price } from "../components/hotelReservation/price" import type { Price } from "../components/hotelReservation/price"
import type { import type { CurrencyEnum } from "../enums/currency"
Child,
SelectRateSearchParams,
} from "../components/hotelReservation/selectRate/selectRate"
import type { Packages } from "../requests/packages" import type { Packages } from "../requests/packages"
export interface InitialRoomData { export interface InitialRoomData {
@@ -78,7 +76,7 @@ export interface RoomState {
} }
export type InitialState = { export type InitialState = {
booking: SelectRateSearchParams booking: DetailsBooking
rooms: InitialRoomData[] rooms: InitialRoomData[]
vat: number vat: number
} }
@@ -92,7 +90,7 @@ export interface DetailsState {
addPreSubmitCallback: (name: string, callback: () => void) => void addPreSubmitCallback: (name: string, callback: () => void) => void
} }
availableBeds: Record<string, number> availableBeds: Record<string, number>
booking: SelectRateSearchParams booking: DetailsBooking
breakfastPackages: BreakfastPackages breakfastPackages: BreakfastPackages
canProceedToPayment: boolean canProceedToPayment: boolean
isSubmitting: boolean isSubmitting: boolean
@@ -107,6 +105,6 @@ export interface DetailsState {
} }
export type PersistedState = { export type PersistedState = {
booking: SelectRateSearchParams booking: DetailsBooking
rooms: RoomState[] rooms: RoomState[]
} }

View File

@@ -2,7 +2,7 @@ import type { DefaultFilterOptions } from "@/types/components/hotelReservation/s
import type { import type {
Rate, Rate,
Room as RoomBooking, Room as RoomBooking,
SelectRateSearchParams, SelectRateBooking,
} from "@/types/components/hotelReservation/selectRate/selectRate" } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { CurrencyEnum } from "@/types/enums/currency" import type { CurrencyEnum } from "@/types/enums/currency"
import type { Room } from "@/types/hotel" import type { Room } from "@/types/hotel"
@@ -54,7 +54,7 @@ export interface SelectedRoom {
export interface RatesState { export interface RatesState {
activeRoom: number activeRoom: number
booking: SelectRateSearchParams booking: SelectRateBooking
hotelType: string | undefined hotelType: string | undefined
isRedemptionBooking: boolean isRedemptionBooking: boolean
packageOptions: DefaultFilterOptions[] packageOptions: DefaultFilterOptions[]

View File

@@ -5,16 +5,9 @@ import { getLocations } from "@/lib/trpc/memoizedRequests"
import { generateChildrenString } from "@/components/HotelReservation/utils" import { generateChildrenString } from "@/components/HotelReservation/utils"
import { safeTry } from "@/utils/safeTry" import { safeTry } from "@/utils/safeTry"
import { type SelectHotelParams } from "@/utils/url"
import type { import type { BookingSearchType } from "@/types/components/hotelReservation/booking"
AlternativeHotelsSearchParams, import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
SelectHotelSearchParams,
} from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import type {
Child,
SelectRateSearchParams,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import { import {
type HotelLocation, type HotelLocation,
isHotelLocation, isHotelLocation,
@@ -24,41 +17,44 @@ import {
export type ChildrenInRoom = (Child[] | null)[] | null export type ChildrenInRoom = (Child[] | null)[] | null
export type ChildrenInRoomString = (string | null)[] | null export type ChildrenInRoomString = (string | null)[] | null
interface HotelSearchDetails<T> { interface HotelSearchDetails {
adultsInRoom: number[] adultsInRoom: number[]
bookingCode?: string bookingCode?: string
childrenInRoom: ChildrenInRoom childrenInRoom: ChildrenInRoom
childrenInRoomString: ChildrenInRoomString childrenInRoomString: ChildrenInRoomString
city: Location | null city: Location | null
cityName: string | undefined
hotel: HotelLocation | null hotel: HotelLocation | null
noOfRooms: number noOfRooms: number
redemption?: boolean redemption?: boolean
selectHotelParams: SelectHotelParams<T> & { city: string | undefined }
} }
export async function getHotelSearchDetails< export async function getHotelSearchDetails(
T extends params: {
| SelectHotelSearchParams hotelId?: string
| SelectRateSearchParams city?: string
| AlternativeHotelsSearchParams, rooms?: {
>( adults: number
selectHotelParams: SelectHotelParams<T>, childrenInRoom?: Child[]
}[]
bookingCode?: string
searchType?: BookingSearchType
},
isAlternativeHotels?: boolean isAlternativeHotels?: boolean
): Promise<HotelSearchDetails<T> | null> { ): Promise<HotelSearchDetails | null> {
const [locations, error] = await safeTry(getLocations()) const [locations, error] = await safeTry(getLocations())
if (!locations || error) { if (!locations || error) {
return null return null
} }
const hotel = const hotel = params.hotelId
("hotelId" in selectHotelParams && ? ((locations.find(
(locations.find(
(location) => (location) =>
isHotelLocation(location) && isHotelLocation(location) &&
"operaId" in location && "operaId" in location &&
location.operaId === selectHotelParams.hotelId location.operaId === params.hotelId
) as HotelLocation | undefined)) || ) as HotelLocation | undefined) ?? null)
null : null
if (isAlternativeHotels && !hotel) { if (isAlternativeHotels && !hotel) {
return notFound() return notFound()
@@ -66,16 +62,13 @@ export async function getHotelSearchDetails<
const cityName = isAlternativeHotels const cityName = isAlternativeHotels
? hotel?.relationships.city.name ? hotel?.relationships.city.name
: "city" in selectHotelParams : params.city
? (selectHotelParams.city as string | undefined)
: undefined
const city = const city = cityName
(typeof cityName === "string" && ? (locations.find(
locations.find(
(location) => location.name.toLowerCase() === cityName.toLowerCase() (location) => location.name.toLowerCase() === cityName.toLowerCase()
)) || ) ?? null)
null : null
if (!city && !hotel) return notFound() if (!city && !hotel) return notFound()
if (isAlternativeHotels && (!city || !hotel)) return notFound() if (isAlternativeHotels && (!city || !hotel)) return notFound()
@@ -84,7 +77,7 @@ export async function getHotelSearchDetails<
let childrenInRoom: ChildrenInRoom = null let childrenInRoom: ChildrenInRoom = null
let childrenInRoomString: ChildrenInRoomString = null let childrenInRoomString: ChildrenInRoomString = null
const { rooms } = selectHotelParams const { rooms } = params
if (rooms?.length) { if (rooms?.length) {
adultsInRoom = rooms.map((room) => room.adults ?? 0) adultsInRoom = rooms.map((room) => room.adults ?? 0)
@@ -97,13 +90,13 @@ export async function getHotelSearchDetails<
return { return {
adultsInRoom, adultsInRoom,
bookingCode: selectHotelParams.bookingCode ?? undefined, bookingCode: params.bookingCode ?? undefined,
childrenInRoom, childrenInRoom,
childrenInRoomString, childrenInRoomString,
city, city,
cityName,
hotel, hotel,
noOfRooms: rooms?.length ?? 0, noOfRooms: rooms?.length ?? 0,
redemption: selectHotelParams.searchType === REDEMPTION, redemption: params.searchType === REDEMPTION,
selectHotelParams: { city: cityName, ...selectHotelParams },
} }
} }

View File

@@ -0,0 +1,509 @@
import { describe, expect, test } from "@jest/globals"
import { z } from "zod"
import { parseSearchParams, serializeSearchParams } from "./searchParams"
describe("Parse search params", () => {
test("with flat values", () => {
const searchParams = getSearchParams("city=stockholm&hotel=123")
const result = parseSearchParams(searchParams)
expect(result).toEqual({
city: "stockholm",
hotel: "123",
})
})
test("with comma separated array", () => {
const searchParams = getSearchParams(
"filter=1831,1383,971,1607&packages=ABC,XYZ"
)
const result = parseSearchParams(searchParams, {
typeHints: {
packages: "COMMA_SEPARATED_ARRAY",
filter: "COMMA_SEPARATED_ARRAY",
},
})
expect(result).toEqual({
filter: ["1831", "1383", "971", "1607"],
packages: ["ABC", "XYZ"],
})
})
test("with comma separated array with single value", () => {
const searchParams = getSearchParams(
"details.packages=ABC&filter=1831&rooms[0].packages=XYZ"
)
const result = parseSearchParams(searchParams, {
typeHints: {
filter: "COMMA_SEPARATED_ARRAY",
packages: "COMMA_SEPARATED_ARRAY",
},
})
expect(result).toEqual({
filter: ["1831"],
details: {
packages: ["ABC"],
},
rooms: [
{
packages: ["XYZ"],
},
],
})
})
test("with nested object", () => {
const searchParams = getSearchParams(
"room.details.adults=1&room.ratecode=ABC&room.details.children=2&room.filters=1,2,3,4"
)
const result = parseSearchParams(searchParams, {
typeHints: {
filters: "COMMA_SEPARATED_ARRAY",
},
})
expect(result).toEqual({
room: {
ratecode: "ABC",
filters: ["1", "2", "3", "4"],
details: {
adults: "1",
children: "2",
},
},
})
})
test("with array of objects", () => {
const searchParams = getSearchParams(
"room[0].adults=1&room[0].ratecode=ABC&room[1].adults=2&room[1].ratecode=DEF"
)
const result = parseSearchParams(searchParams)
expect(result).toEqual({
room: [
{
adults: "1",
ratecode: "ABC",
},
{
adults: "2",
ratecode: "DEF",
},
],
})
})
test("with array defined out of order", () => {
const searchParams = getSearchParams("room[1].adults=1&room[0].adults=2")
const result = parseSearchParams(searchParams)
expect(result).toEqual({
room: [
{
adults: "2",
},
{
adults: "1",
},
],
})
})
test("with nested array of objects", () => {
const searchParams = getSearchParams(
"room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
)
const result = parseSearchParams(searchParams)
expect(result).toEqual({
room: [
{
adults: "1",
child: [
{
age: "2",
},
],
},
{
adults: "2",
child: [
{
age: "3",
},
],
},
],
})
})
test("can handle array syntax with primitive values", () => {
const searchParams = getSearchParams("room[1]=1&room[0]=2")
const result = parseSearchParams(searchParams)
expect(result).toEqual({
room: ["2", "1"],
})
})
test("can rename search param keys", () => {
const searchParams = getSearchParams(
"city=stockholm&hotel=123&room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
)
const result = parseSearchParams(searchParams, {
keyRenameMap: {
hotel: "hotelId",
room: "rooms",
age: "childAge",
},
})
expect(result).toEqual({
city: "stockholm",
hotelId: "123",
rooms: [
{
adults: "1",
child: [
{
childAge: "2",
},
],
},
{
adults: "2",
child: [
{
childAge: "3",
},
],
},
],
})
})
test("with schema validation", () => {
const searchParams = getSearchParams(
"room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
)
const result = parseSearchParams(searchParams, {
schema: z.object({
room: z.array(
z.object({
adults: z.string(),
child: z.array(
z.object({
age: z.string(),
})
),
})
),
}),
})
expect(result).toEqual({
room: [
{
adults: "1",
child: [
{
age: "2",
},
],
},
{
adults: "2",
child: [
{
age: "3",
},
],
},
],
})
})
test("throws when schema validation fails", () => {
const searchParams = getSearchParams("city=stockholm")
expect(() =>
parseSearchParams(searchParams, {
schema: z.object({
city: z.string(),
hotel: z.string(),
}),
})
).toThrow()
})
test("with value coercion", () => {
const searchParams = getSearchParams(
"room[0].adults=1&room[0].enabled=true"
)
const result = parseSearchParams(searchParams, {
schema: z.object({
room: z.array(
z.object({
adults: z.coerce.number(),
enabled: z.coerce.boolean(),
})
),
}),
})
expect(result).toEqual({
room: [
{
adults: 1,
enabled: true,
},
],
})
})
})
describe("Serialize search params", () => {
test("with flat values", () => {
const obj = {
city: "stockholm",
hotel: "123",
}
const result = serializeSearchParams(obj)
expect(decodeURIComponent(result.toString())).toEqual(
"city=stockholm&hotel=123"
)
})
test("with comma separated array", () => {
const obj = {
filter: ["1831", "1383", "971", "1607"],
}
const result = serializeSearchParams(obj, {
typeHints: {
filter: "COMMA_SEPARATED_ARRAY",
},
})
expect(decodeURIComponent(result.toString())).toEqual(
"filter=1831,1383,971,1607"
)
})
test("with comma separated array with single value", () => {
const obj = {
details: {
packages: ["ABC"],
},
filter: ["1831"],
rooms: [
{
packages: ["XYZ"],
},
],
}
const result = serializeSearchParams(obj, {
typeHints: {
filter: "COMMA_SEPARATED_ARRAY",
packages: "COMMA_SEPARATED_ARRAY",
},
})
expect(decodeURIComponent(result.toString())).toEqual(
"details.packages=ABC&filter=1831&rooms[0].packages=XYZ"
)
})
test("with nested object", () => {
const obj = {
room: {
ratecode: "ABC",
filters: ["1", "2", "3", "4"],
details: {
adults: "1",
children: "2",
},
},
}
const result = serializeSearchParams(obj, {
typeHints: {
filters: "COMMA_SEPARATED_ARRAY",
},
})
expect(decodeURIComponent(result.toString())).toEqual(
"room.ratecode=ABC&room.filters=1,2,3,4&room.details.adults=1&room.details.children=2"
)
})
test("with array of objects", () => {
const obj = {
room: [
{
adults: "1",
ratecode: "ABC",
},
{
adults: "2",
ratecode: "DEF",
},
],
}
const result = serializeSearchParams(obj)
expect(decodeURIComponent(result.toString())).toEqual(
"room[0].adults=1&room[0].ratecode=ABC&room[1].adults=2&room[1].ratecode=DEF"
)
})
test("with nested array of objects", () => {
const obj = {
room: [
{
adults: "1",
child: [
{
age: "2",
},
],
},
{
adults: "2",
child: [
{
age: "3",
},
],
},
],
}
const result = serializeSearchParams(obj)
expect(decodeURIComponent(result.toString())).toEqual(
"room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
)
})
test("can handle array syntax with primitive values", () => {
const obj = {
room: ["2", "1"],
}
const result = serializeSearchParams(obj)
expect(decodeURIComponent(result.toString())).toEqual("room[0]=2&room[1]=1")
})
test("can rename search param keys", () => {
const obj = {
city: "stockholm",
hotelId: "123",
rooms: [
{
adults: "1",
child: [
{
childAge: "2",
},
],
},
{
adults: "2",
child: [
{
childAge: "3",
},
],
},
],
}
const result = serializeSearchParams(obj, {
keyRenameMap: {
hotelId: "hotel",
rooms: "room",
childAge: "age",
},
})
expect(decodeURIComponent(result.toString())).toEqual(
"city=stockholm&hotel=123&room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
)
})
test("with initial search params", () => {
const initialSearchParams = new URLSearchParams("city=stockholm&hotel=123")
const obj = {
hotel: "456",
filter: ["1831", "1383"],
packages: ["ABC"],
}
const result = serializeSearchParams(obj, {
initialSearchParams,
typeHints: {
packages: "COMMA_SEPARATED_ARRAY",
},
})
expect(decodeURIComponent(result.toString())).toEqual(
"city=stockholm&hotel=456&filter[0]=1831&filter[1]=1383&packages=ABC"
)
})
})
describe("Parse serialized search params", () => {
test("should return the same object", () => {
const obj = {
city: "stockholm",
hotelId: "123",
filter: ["1831", "1383", "971", "1607"],
details: {
packages: ["ABC"],
},
rooms: [
{
packages: ["XYZ"],
},
],
}
const searchParams = serializeSearchParams(obj, {
keyRenameMap: {
hotelId: "hotel",
rooms: "room",
},
typeHints: {
filter: "COMMA_SEPARATED_ARRAY",
packages: "COMMA_SEPARATED_ARRAY",
},
})
const searchParamsObj = searchParamsToObject(searchParams)
const result = parseSearchParams(searchParamsObj, {
keyRenameMap: {
hotel: "hotelId",
room: "rooms",
},
typeHints: {
filter: "COMMA_SEPARATED_ARRAY",
packages: "COMMA_SEPARATED_ARRAY",
},
})
expect(result).toEqual(obj)
})
})
// Simulates what Next does behind the scenes for search params
const getSearchParams = (input: string) => {
const searchParams = new URLSearchParams(input)
return searchParamsToObject(searchParams)
}
const searchParamsToObject = (searchParams: URLSearchParams) => {
const obj: Record<string, any> = {}
for (const [key, value] of searchParams.entries()) {
obj[key] = value
}
return obj
}

View File

@@ -0,0 +1,209 @@
import type { z } from "zod"
import type { NextSearchParams } from "@/types/params"
type ParseOptions<T extends z.ZodRawShape> = {
keyRenameMap?: Record<string, string>
typeHints?: Record<string, "COMMA_SEPARATED_ARRAY">
schema?: z.ZodObject<T>
}
type ParseOptionsWithSchema<T extends z.ZodRawShape> = ParseOptions<T> & {
schema: z.ZodObject<T>
}
// This ensures that the return type is correct when a schema is provided
export function parseSearchParams<T extends z.ZodRawShape>(
searchParams: NextSearchParams,
options: ParseOptionsWithSchema<T>
): z.infer<typeof options.schema>
export function parseSearchParams<T extends z.ZodRawShape>(
searchParams: NextSearchParams,
options?: ParseOptions<T>
): Record<string, any>
/**
* Parses URL search parameters into a structured object.
* This function can handle nested objects, arrays, and type validation/transformation using Zod schema.
*
* @param searchParams - The object to parse
* @param options.keyRenameMap - Optional mapping of keys to rename, ie { "oldKey": "newKey" }
* @param options.typeHints - Optional type hints to force certain keys to be treated as arrays
* @param options.schema - Pass a Zod schema to validate and transform the parsed search parameters and get a typed return value
*
* Supported formats:
* - Objects: `user.name=John&user.age=30`
* - Arrays: `tags[0]=javascript&tags[1]=typescript`
* - Arrays of objects: `tags[0].name=javascript&tags[0].age=30`
* - Nested arrays: `tags[0].languages[0]=javascript&tags[0].languages[1]=typescript`
* - Comma-separated arrays: `tags=javascript,typescript`
*
* For comma-separated arrays you must use the `typeHints`
* option to inform the parser that the key should be treated as an array.
*/
export function parseSearchParams<T extends z.ZodRawShape>(
searchParams: NextSearchParams,
options?: ParseOptions<T>
) {
const entries = Object.entries(searchParams)
const buildObject = getBuilder(options || {})
const resultObject: Record<string, any> = {}
for (const [key, value] of entries) {
const paths = key.split(".")
if (Array.isArray(value)) {
throw new Error(
`Arrays from duplicate keys (?a=1&a=2) are not yet supported.`
)
}
if (!value) {
continue
}
buildObject(resultObject, paths, value)
}
if (options?.schema) {
return options.schema.parse(resultObject)
}
return resultObject
}
// Use a higher-order function to avoid passing the options
// object every time we recursively call the builder
function getBuilder<T extends z.ZodRawShape>(options: ParseOptions<T>) {
const keyRenameMap = options.keyRenameMap || {}
const typeHints = options.typeHints || {}
return function buildNestedObject(
obj: Record<string, any>,
paths: string[],
value: string
) {
if (paths.length === 0) return
const path = paths[0]
const remainingPaths = paths.slice(1)
// Extract the key name and optional array index
const match = path.match(/^([^\[]+)(?:\[(\d+)\])?$/)
if (!match) return
const key = keyRenameMap[match[1]] || match[1]
const index = match[2] ? parseInt(match[2]) : null
const forceCommaSeparatedArray = typeHints[key] === "COMMA_SEPARATED_ARRAY"
const hasIndex = index !== null
// If we've reached the last path, set the value
if (remainingPaths.length === 0) {
// This is either an array or a value that is
// forced to be an array by the typeHints
if (hasIndex || forceCommaSeparatedArray) {
if (isNotArray(obj[key])) obj[key] = []
if (!hasIndex || forceCommaSeparatedArray) {
obj[key] = value.split(",")
return
}
obj[key][index] = value
return
}
obj[key] = value
return
}
if (hasIndex) {
// If the key is an array, ensure array and element at index exists
if (isNotArray(obj[key])) obj[key] = []
if (!obj[key][index]) obj[key][index] = {}
buildNestedObject(obj[key][index], remainingPaths, value)
return
}
// Otherwise, it should be an object
if (!obj[key]) obj[key] = {}
buildNestedObject(obj[key], remainingPaths, value)
}
}
function isNotArray(value: any) {
return !value || typeof value !== "object" || !Array.isArray(value)
}
type SerializeOptions = {
keyRenameMap?: Record<string, string>
typeHints?: Record<string, "COMMA_SEPARATED_ARRAY">
initialSearchParams?: URLSearchParams
}
/**
* Serializes an object into URL search parameters.
*
* @param obj - The object to serialize
* @param options.keyRenameMap - Optional mapping of keys to rename, ie { "oldKey": "newKey" }
* @param options.typeHints - Optional type hints to force certain keys to be treated as comma separated arrays
* @returns URLSearchParams - The serialized URL search parameters
*/
export function serializeSearchParams(
obj: Record<string, any>,
options?: SerializeOptions
): URLSearchParams {
const params = new URLSearchParams(options?.initialSearchParams)
const keyRenameMap = options?.keyRenameMap || {}
const typeHints = options?.typeHints || {}
function buildParams(obj: unknown, prefix: string) {
if (obj === null || obj === undefined) return
if (!isRecord(obj)) {
params.set(prefix, String(obj))
return
}
for (const key in obj) {
const value = obj[key]
const renamedKey = keyRenameMap[key] || key
if (Array.isArray(value)) {
if (typeHints[key] === "COMMA_SEPARATED_ARRAY") {
const paramKey = prefix ? `${prefix}.${renamedKey}` : renamedKey
params.set(paramKey, value.join(","))
continue
}
value.forEach((item, index) => {
const indexedKey = `${renamedKey}[${index}]`
const paramKey = prefix ? `${prefix}.${indexedKey}` : indexedKey
buildParams(item, paramKey)
})
continue
}
const paramKey = prefix ? `${prefix}.${renamedKey}` : renamedKey
if (typeof value === "object" && value !== null) {
buildParams(value, paramKey)
continue
}
params.set(paramKey, String(value))
}
}
buildParams(obj, "")
return params
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}

View File

@@ -1,10 +1,20 @@
import { z } from "zod"
import { bookingSearchTypes } from "@/constants/booking"
import { Lang } from "@/constants/languages" import { Lang } from "@/constants/languages"
import type { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { parseSearchParams, serializeSearchParams } from "./searchParams"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget"
import type { DetailsBooking } from "@/types/components/hotelReservation/enterDetails/details"
import type { SelectHotelBooking } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { import type {
Child,
Room, Room,
SelectRateBooking,
} from "@/types/components/hotelReservation/selectRate/selectRate" } from "@/types/components/hotelReservation/selectRate/selectRate"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { NextSearchParams } from "@/types/params"
export function removeMultipleSlashes(pathname: string) { export function removeMultipleSlashes(pathname: string) {
return pathname.replaceAll(/\/\/+/g, "/") return pathname.replaceAll(/\/\/+/g, "/")
@@ -20,151 +30,213 @@ export function removeTrailingSlash(pathname: string) {
type PartialRoom = { rooms?: Partial<Room>[] } type PartialRoom = { rooms?: Partial<Room>[] }
const keyedSearchParams = new Map([
["room", "rooms"],
["ratecode", "rateCode"],
["counterratecode", "counterRateCode"],
["roomtype", "roomTypeCode"],
["fromdate", "fromDate"],
["todate", "toDate"],
["hotel", "hotelId"],
["child", "childrenInRoom"],
["searchtype", "searchType"],
])
export type SelectHotelParams<T> = Omit<T, "hotel"> & { export type SelectHotelParams<T> = Omit<T, "hotel"> & {
hotelId: string hotelId: string
} & PartialRoom } & PartialRoom
export function getKeyFromSearchParam(key: string): string {
return keyedSearchParams.get(key) || key
}
export function getSearchParamFromKey(key: string): string {
for (const [mapKey, mapValue] of keyedSearchParams.entries()) {
if (mapValue === key) {
return mapKey
}
}
return key
}
export function searchParamsToRecord(searchParams: URLSearchParams) { export function searchParamsToRecord(searchParams: URLSearchParams) {
return Object.fromEntries(searchParams.entries()) return Object.fromEntries(searchParams.entries())
} }
export function convertSearchParamsToObj<T extends PartialRoom>( const keyRenameMap = {
searchParams: Record<string, string> room: "rooms",
): SelectHotelParams<T> { ratecode: "rateCode",
const searchParamsObject = Object.entries(searchParams).reduce< counterratecode: "counterRateCode",
SelectHotelParams<T> roomtype: "roomTypeCode",
>((acc, [key, value]) => { fromdate: "fromDate",
// The params are sometimes indexed with a number (for ex: `room[0].adults`), todate: "toDate",
// so we need to split them by . or [] hotel: "hotelId",
const keys = key.replace(/\]/g, "").split(/\[|\./) child: "childrenInRoom",
const firstKey = getKeyFromSearchParam(keys[0]) searchtype: "searchType",
}
const adultsSchema = z.coerce.number().min(1).max(6).catch(0)
const childAgeSchema = z.coerce.number().catch(-1)
const childBedSchema = z.coerce.number().catch(-1)
const searchTypeSchema = z.enum(bookingSearchTypes).optional().catch(undefined)
// Room is a special case since it is an array, so we need to handle it separately export function parseBookingWidgetSearchParams(
if (firstKey === "rooms") { searchParams: NextSearchParams
// Rooms are always indexed with a number, so we need to extract the index ): BookingWidgetSearchData {
const index = Number(keys[1]) try {
const roomObject = const result = parseSearchParams(searchParams, {
acc.rooms && Array.isArray(acc.rooms) ? acc.rooms : (acc.rooms = []) keyRenameMap,
schema: z.object({
city: z.string().optional(),
hotelId: z.string().optional(),
fromDate: z.string().optional(),
toDate: z.string().optional(),
bookingCode: z.string().optional(),
searchType: searchTypeSchema,
rooms: z
.array(
z.object({
adults: adultsSchema,
childrenInRoom: z
.array(
z.object({
bed: childBedSchema,
age: childAgeSchema,
})
)
.optional()
.default([]),
})
)
.optional(),
}),
})
const roomObjectKey = getKeyFromSearchParam(keys[2]) as keyof Room return result
} catch (error) {
if (!roomObject[index]) { console.log("[URL] Error parsing search params for booking widget:", error)
roomObject[index] = {} return {}
} }
// Adults should be converted to a number
if (roomObjectKey === "adults") {
roomObject[index].adults = Number(value)
// Child is an array, so we need to handle it separately
} else if (roomObjectKey === "childrenInRoom") {
const childIndex = Number(keys[3])
const childKey = keys[4] as keyof Child
if (
!("childrenInRoom" in roomObject[index]) ||
!Array.isArray(roomObject[index].childrenInRoom)
) {
roomObject[index].childrenInRoom = []
}
roomObject[index].childrenInRoom![childIndex] = {
...roomObject[index].childrenInRoom![childIndex],
[childKey]: Number(value),
}
} else if (roomObjectKey === "packages") {
roomObject[index].packages = value.split(",") as RoomPackageCodeEnum[]
} else {
roomObject[index][roomObjectKey] = value
}
} else {
return { ...acc, [firstKey]: value }
}
return acc
}, {} as SelectHotelParams<T>)
return searchParamsObject
} }
export function convertObjToSearchParams<T>( export function parseSelectHotelSearchParams(
bookingData: T & PartialRoom, searchParams: NextSearchParams
intitalSearchParams = {} as URLSearchParams ): SelectHotelBooking | null {
) { try {
const bookingSearchParams = new URLSearchParams(intitalSearchParams) const result = parseSearchParams(searchParams, {
Object.entries(bookingData).forEach(([key, value]) => { keyRenameMap,
if (key === "rooms") { schema: z.object({
value.forEach((item, index) => { city: z.string(),
if (item?.adults) { hotelId: z.string().optional(),
bookingSearchParams.set( fromDate: z.string(),
`room[${index}].adults`, toDate: z.string(),
item.adults.toString() bookingCode: z.string().optional(),
) searchType: searchTypeSchema,
} rooms: z.array(
if (item?.childrenInRoom) { z.object({
item.childrenInRoom.forEach((child, childIndex) => { adults: adultsSchema,
bookingSearchParams.set( childrenInRoom: z
`room[${index}].child[${childIndex}].age`, .array(
child.age.toString() z.object({
) bed: childBedSchema,
bookingSearchParams.set( age: childAgeSchema,
`room[${index}].child[${childIndex}].bed`, })
child.bed.toString() )
) .optional(),
}) })
} ),
if (item?.roomTypeCode) { }),
bookingSearchParams.set(`room[${index}].roomtype`, item.roomTypeCode) })
}
if (item?.rateCode) {
bookingSearchParams.set(`room[${index}].ratecode`, item.rateCode)
}
if (item?.counterRateCode) { return result
bookingSearchParams.set( } catch (error) {
`room[${index}].counterratecode`, console.log("[URL] Error parsing search params for select hotel:", error)
item.counterRateCode
)
}
if (item.packages && item.packages.length > 0) { return null
bookingSearchParams.set( }
`room[${index}].packages`, }
item.packages.join(",")
) export function parseSelectRateSearchParams(
} searchParams: NextSearchParams
}) ): SelectRateBooking | null {
} else { try {
bookingSearchParams.set(getSearchParamFromKey(key), value.toString()) const result = parseSearchParams(searchParams, {
} keyRenameMap,
schema: z.object({
city: z.string().optional(),
hotelId: z.string(),
fromDate: z.string(),
toDate: z.string(),
searchType: searchTypeSchema,
bookingCode: z.string().optional(),
rooms: z.array(
z.object({
adults: adultsSchema,
bookingCode: z.string().optional(),
counterRateCode: z.string().optional(),
rateCode: z.string().optional(),
roomTypeCode: z.string().optional(),
packages: z
.array(
z.nativeEnum({
...BreakfastPackageEnum,
...RoomPackageCodeEnum,
})
)
.optional(),
childrenInRoom: z
.array(
z.object({
bed: childBedSchema,
age: childAgeSchema,
})
)
.optional(),
})
),
}),
})
return result
} catch (error) {
console.log("[URL] Error parsing search params for select rate:", error)
return null
}
}
export function parseDetailsSearchParams(
searchParams: NextSearchParams
): DetailsBooking | null {
const packageEnum = {
...BreakfastPackageEnum,
...RoomPackageCodeEnum,
} as const
try {
const result = parseSearchParams(searchParams, {
keyRenameMap,
schema: z.object({
city: z.string().optional(),
hotelId: z.string(),
fromDate: z.string(),
toDate: z.string(),
searchType: searchTypeSchema,
bookingCode: z.string().optional(),
rooms: z.array(
z.object({
adults: adultsSchema,
bookingCode: z.string().optional(),
counterRateCode: z.string().optional(),
rateCode: z.string(),
roomTypeCode: z.string(),
packages: z.array(z.nativeEnum(packageEnum)).optional(),
childrenInRoom: z
.array(
z.object({
bed: childBedSchema,
age: childAgeSchema,
})
)
.optional(),
})
),
}),
})
return result
} catch (error) {
console.log("[URL] Error parsing search params for details:", error)
return null
}
}
const reversedKeyRenameMap = Object.fromEntries(
Object.entries(keyRenameMap).map(([key, value]) => [value, key])
)
export function serializeBookingSearchParams(
obj: { [key: string]: any },
{ initialSearchParams }: { initialSearchParams?: URLSearchParams } = {}
) {
return serializeSearchParams(obj, {
keyRenameMap: reversedKeyRenameMap,
initialSearchParams,
}) })
return bookingSearchParams
} }
/** /**