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
@@ -19,7 +19,6 @@ import useLang from "@/hooks/useLang"
import useStickyPosition from "@/hooks/useStickyPosition"
import { debounce } from "@/utils/debounce"
import isValidJson from "@/utils/isValidJson"
import { convertSearchParamsToObj } from "@/utils/url"
import MobileToggleButton, {
MobileToggleButtonSkeleton,
@@ -35,12 +34,11 @@ import type {
BookingCodeSchema,
BookingWidgetClientProps,
BookingWidgetSchema,
BookingWidgetSearchData,
} from "@/types/components/bookingWidget"
export default function BookingWidgetClient({
type,
bookingWidgetSearchParams,
data,
pageSettingsBookingCodePromise,
}: BookingWidgetClientProps) {
const [isOpen, setIsOpen] = useState(false)
@@ -50,22 +48,19 @@ export default function BookingWidgetClient({
null
)
const params = convertSearchParamsToObj<BookingWidgetSearchData>(
bookingWidgetSearchParams
)
const shouldFetchAutoComplete = !!data.hotelId || !!data.city
const shouldFetchAutoComplete = !!params.hotelId || !!params.city
const { data, isPending } = trpc.autocomplete.destinations.useQuery(
{
lang,
query: "",
includeTypes: ["hotels", "cities"],
selectedHotelId: params.hotelId,
selectedCity: params.city,
},
{ enabled: shouldFetchAutoComplete }
)
const { data: destinationsData, isPending } =
trpc.autocomplete.destinations.useQuery(
{
lang,
query: "",
includeTypes: ["hotels", "cities"],
selectedHotelId: data.hotelId ? data.hotelId.toString() : undefined,
selectedCity: data.city,
},
{ enabled: shouldFetchAutoComplete }
)
const shouldShowSkeleton = shouldFetchAutoComplete && isPending
useStickyPosition({
@@ -76,8 +71,8 @@ export default function BookingWidgetClient({
const now = dt()
// 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.
let fromDate = dt(params.fromDate)
let toDate = dt(params.toDate)
let fromDate = dt(data.fromDate)
let toDate = dt(data.toDate)
const isDateParamValid =
fromDate.isValid() &&
@@ -91,17 +86,18 @@ export default function BookingWidgetClient({
}
let selectedLocation =
data?.currentSelection.hotel ?? data?.currentSelection.city
destinationsData?.currentSelection.hotel ??
destinationsData?.currentSelection.city
// if bookingCode is not provided in the search params,
// we will fetch it from the page settings stored in Contentstack.
const selectedBookingCode =
params.bookingCode ||
data.bookingCode ||
(pageSettingsBookingCodePromise !== null
? use(pageSettingsBookingCodePromise)
: "")
const defaultRoomsData: BookingWidgetSchema["rooms"] = params.rooms?.map(
const defaultRoomsData: BookingWidgetSchema["rooms"] = data.rooms?.map(
(room) => ({
adults: room.adults,
childrenInRoom: room.childrenInRoom || [],
@@ -112,7 +108,7 @@ export default function BookingWidgetClient({
childrenInRoom: [],
},
]
const hotelId = isNaN(+params.hotelId) ? undefined : +params.hotelId
const hotelId = data.hotelId ? parseInt(data.hotelId) : undefined
const methods = useForm({
defaultValues: {
search: selectedLocation?.name ?? "",
@@ -126,9 +122,9 @@ export default function BookingWidgetClient({
value: selectedBookingCode,
remember: false,
},
redemption: params?.searchType === REDEMPTION,
redemption: data.searchType === REDEMPTION,
rooms: defaultRoomsData,
city: params.city || undefined,
city: data.city || undefined,
hotel: hotelId,
},
shouldFocusError: false,
@@ -8,7 +8,7 @@ import { FloatingBookingWidgetClient } from "./FloatingBookingWidgetClient"
import type { BookingWidgetProps } from "@/types/components/bookingWidget"
export async function FloatingBookingWidget({
bookingWidgetSearchParams,
booking,
}: Omit<BookingWidgetProps, "type">) {
const isHidden = await isBookingWidgetHidden()
@@ -17,13 +17,13 @@ export async function FloatingBookingWidget({
}
let pageSettingsBookingCodePromise: Promise<string> | null = null
if (!bookingWidgetSearchParams.bookingCode) {
if (!booking.bookingCode) {
pageSettingsBookingCodePromise = getPageSettingsBookingCode()
}
return (
<FloatingBookingWidgetClient
bookingWidgetSearchParams={bookingWidgetSearchParams}
data={booking}
pageSettingsBookingCodePromise={pageSettingsBookingCodePromise}
/>
)
@@ -17,10 +17,7 @@ export async function BookingWidget(props: BookingWidgetProps) {
)
}
async function InternalBookingWidget({
type,
bookingWidgetSearchParams,
}: BookingWidgetProps) {
async function InternalBookingWidget({ type, booking }: BookingWidgetProps) {
const isHidden = await isBookingWidgetHidden()
if (isHidden) {
@@ -28,14 +25,14 @@ async function InternalBookingWidget({
}
let pageSettingsBookingCodePromise: Promise<string> | null = null
if (!bookingWidgetSearchParams.bookingCode) {
if (!booking.bookingCode) {
pageSettingsBookingCodePromise = getPageSettingsBookingCode()
}
return (
<BookingWidgetClient
type={type}
bookingWidgetSearchParams={bookingWidgetSearchParams}
data={booking}
pageSettingsBookingCodePromise={pageSettingsBookingCodePromise}
/>
)
@@ -9,12 +9,13 @@ import TrackingSDK from "@/components/TrackingSDK"
import styles from "./startPage.module.css"
import type { BookingWidgetSearchData } from "@/types/components/bookingWidget"
import { BlocksEnums } from "@/types/enums/blocks"
export default async function StartPage({
searchParams,
booking,
}: {
searchParams: { [key: string]: string }
booking: BookingWidgetSearchData
}) {
const content = await getStartPage()
if (!content) {
@@ -30,7 +31,7 @@ export default async function StartPage({
<Title color="white" textAlign="center">
{header.heading}
</Title>
<FloatingBookingWidget bookingWidgetSearchParams={searchParams} />
<FloatingBookingWidget booking={booking} />
</div>
{header.hero_image ? (
<Image
@@ -9,7 +9,7 @@ import { REDEMPTION } from "@/constants/booking"
import { selectHotel, selectRate } from "@/constants/routes/hotelReservation"
import useLang from "@/hooks/useLang"
import { convertObjToSearchParams } from "@/utils/url"
import { serializeBookingSearchParams } from "@/utils/url"
import FormContent, { BookingWidgetFormContentSkeleton } from "./FormContent"
import { bookingWidgetVariants } from "./variants"
@@ -38,7 +38,7 @@ export default function Form({ type, onClose }: BookingWidgetFormProps) {
function onSubmit(data: BookingWidgetSchema) {
const bookingFlowPage = data.hotel ? selectRate(lang) : selectHotel(lang)
const bookingWidgetParams = convertObjToSearchParams({
const bookingWidgetParams = serializeBookingSearchParams({
rooms: data.rooms,
...data.date,
...(data.city ? { city: data.city } : {}),
@@ -9,7 +9,7 @@ import { detailsStorageName } from "@/stores/enter-details"
import LoadingSpinner from "@/components/LoadingSpinner"
import { trackPaymentEvent } from "@/utils/tracking"
import { trackEvent } from "@/utils/tracking/base"
import { convertObjToSearchParams } from "@/utils/url"
import { serializeBookingSearchParams } from "@/utils/url"
import { clearGlaSessionStorage, readGlaFromSessionStorage } from "./helpers"
@@ -33,9 +33,11 @@ export default function HandleErrorCallback({
if (bookingData) {
const detailsStorage: PersistedState = JSON.parse(bookingData)
const searchParams = convertObjToSearchParams(
const searchParams = serializeBookingSearchParams(
detailsStorage.booking,
searchObject
{
initialSearchParams: searchObject,
}
)
const glaSessionData = readGlaFromSessionStorage()
@@ -7,14 +7,13 @@ import { useSearchHistory } from "@/hooks/useSearchHistory"
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 { Room } from "@/types/providers/details/room"
import type { Lang } from "@/constants/languages"
import type { SelectHotelParams } from "@/utils/url"
interface TrackingWrapperProps {
booking: SelectHotelParams<SelectRateSearchParams>
booking: DetailsBooking
hotel: Hotel
rooms: Room[]
isMember: boolean
@@ -7,8 +7,8 @@ import { getSpecialRoomType } from "@/utils/specialRoomType"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
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 type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import {
TrackingChannelEnum,
type TrackingSDKAncillaries,
@@ -25,10 +25,9 @@ import type {
Product,
} from "@/types/trpc/routers/hotel/roomAvailability"
import type { Lang } from "@/constants/languages"
import type { SelectHotelParams } from "@/utils/url"
export function getTracking(
booking: SelectHotelParams<SelectRateSearchParams>,
booking: DetailsBooking,
hotel: Hotel,
rooms: Room[],
isMember: boolean,
@@ -37,23 +37,25 @@ export async function SelectHotelMapContainer({
bookingCode,
childrenInRoom,
city,
cityName,
hotel: isAlternativeFor,
noOfRooms,
redemption,
selectHotelParams,
} = searchDetails
if (!city) {
return notFound()
}
const hotels = await getHotels(
selectHotelParams,
const hotels = await getHotels({
fromDate: booking.fromDate,
toDate: booking.toDate,
rooms: booking.rooms,
isAlternativeFor,
bookingCode,
city,
!!redemption
)
redemption: !!redemption,
})
const hotelPins = getHotelPins(hotels)
const filterList = getFiltersFromHotels(hotels)
@@ -62,8 +64,8 @@ export async function SelectHotelMapContainer({
hotel: { address: hotels?.[0]?.hotel?.address.streetAddress },
})
const arrivalDate = new Date(selectHotelParams.fromDate)
const departureDate = new Date(selectHotelParams.toDate)
const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(booking.toDate)
const isRedemptionAvailability = redemption
? hotels.some(
(hotel) => hotel.availability.productType?.redemptions?.length
@@ -83,11 +85,11 @@ export async function SelectHotelMapContainer({
adultsInRoom,
childrenInRoom,
hotels.length,
selectHotelParams.hotelId,
booking.hotelId,
noOfRooms,
hotels?.[0]?.hotel.address.country,
hotels?.[0]?.hotel.address.city,
selectHotelParams.city,
cityName,
bookingCode,
isBookingCodeRateAvailable,
redemption,
@@ -18,7 +18,7 @@ export function getTracking(
adultsInRoom: number[],
childrenInRoom: ChildrenInRoom,
hotelsResult: number,
hotelId: string,
hotelId: string | undefined,
noOfRooms: number,
country: string | undefined,
hotelCity: string | undefined,
@@ -14,17 +14,13 @@ import type {
HotelFilter,
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type {
AlternativeHotelsSearchParams,
SelectHotelSearchParams,
} from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { AdditionalData, Hotel } from "@/types/hotel"
import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability"
import type {
HotelLocation,
Location,
} from "@/types/trpc/routers/hotel/locations"
import type { SelectHotelParams } from "@/utils/url"
interface AvailabilityResponse {
availability: HotelsAvailabilityItem[]
@@ -162,19 +158,32 @@ function sortAndFilterHotelsByAvailability(
].flat()
}
export async function getHotels(
booking: SelectHotelParams<
SelectHotelSearchParams | AlternativeHotelsSearchParams
>,
isAlternativeFor: HotelLocation | null,
bookingCode: string | undefined,
city: Location,
type GetHotelsInput = {
fromDate: string
toDate: string
rooms: {
adults: number
childrenInRoom?: Child[]
}[]
isAlternativeFor: HotelLocation | null
bookingCode: string | undefined
city: Location
redemption: boolean
) {
}
export async function getHotels({
rooms,
fromDate,
toDate,
isAlternativeFor,
bookingCode,
city,
redemption,
}: GetHotelsInput) {
let availableHotelsResponse: SettledResult = []
if (isAlternativeFor) {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(async (room) => {
rooms.map(async (room) => {
return fetchAlternativeHotels(isAlternativeFor.id, {
adults: room.adults,
bookingCode,
@@ -182,14 +191,14 @@ export async function getHotels(
? generateChildrenString(room.childrenInRoom)
: undefined,
redemption,
roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate,
roomStayEndDate: toDate,
roomStayStartDate: fromDate,
})
})
)
} else if (bookingCode) {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(async (room) => {
rooms.map(async (room) => {
return fetchBookingCodeAvailableHotels({
adults: room.adults,
bookingCode,
@@ -197,14 +206,14 @@ export async function getHotels(
? generateChildrenString(room.childrenInRoom)
: undefined,
cityId: city.id,
roomStayStartDate: booking.fromDate,
roomStayEndDate: booking.toDate,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
})
})
)
} else {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(
rooms.map(
async (room) =>
await fetchAvailableHotels({
adults: room.adults,
@@ -213,8 +222,8 @@ export async function getHotels(
: undefined,
cityId: city.id,
redemption,
roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate,
roomStayEndDate: toDate,
roomStayStartDate: fromDate,
})
)
)
@@ -17,7 +17,7 @@ export function getTracking(
adultsInRoom: number[],
childrenInRoom: ChildrenInRoom,
hotelsResult: number,
hotelId: string,
hotelId: string | undefined,
noOfRooms: number,
country: string | undefined,
hotelCity: string | undefined,
@@ -72,7 +72,7 @@ export default function Summary({
)
const showDiscounted = containsBookingCodeRate || isMember
const priceDetailsRooms = mapToPrice(rateSummary, booking, isMember)
const priceDetailsRooms = mapToPrice(rateSummary, booking.rooms, isMember)
return (
<section className={styles.summary}>
@@ -1,12 +1,12 @@
import type {
Rate,
SelectRateSearchParams,
Room as SelectRateRoom,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Room } from "@/components/HotelReservation/PriceDetailsModal/PriceDetailsTable"
export function mapToPrice(
rooms: (Rate | null)[],
booking: SelectRateSearchParams,
bookingRooms: SelectRateRoom[],
isUserLoggedIn: boolean
) {
return rooms
@@ -43,7 +43,7 @@ export function mapToPrice(
}
}
const bookingRoom = booking.rooms[idx]
const bookingRoom = bookingRooms[idx]
return {
adults: bookingRoom.adults,
bedType: undefined,
@@ -1,6 +1,6 @@
"use client"
import { useSearchParams } from "next/navigation"
import { notFound, useSearchParams } from "next/navigation"
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
@@ -9,7 +9,7 @@ import { selectRateRoomsAvailabilityInputSchema } from "@/server/routers/hotels/
import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
import RatesProvider from "@/providers/RatesProvider"
import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url"
import { parseSelectRateSearchParams, searchParamsToRecord } from "@/utils/url"
import RateSummary from "./RateSummary"
import Rooms from "./Rooms"
@@ -18,7 +18,6 @@ import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
import styles from "./index.module.css"
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { AlertTypeEnum } from "@/types/enums/alert"
export function RoomsContainer({
@@ -30,10 +29,12 @@ export function RoomsContainer({
const intl = useIntl()
const searchParams = useSearchParams()
const booking = convertSearchParamsToObj<SelectRateSearchParams>(
const booking = parseSelectRateSearchParams(
searchParamsToRecord(searchParams)
)
if (!booking) return notFound()
const bookingInput = selectRateRoomsAvailabilityInputSchema.safeParse({
booking,
lang,
@@ -7,12 +7,11 @@ import { REDEMPTION } from "@/constants/booking"
import TrackingSDK from "@/components/TrackingSDK"
import useLang from "@/hooks/useLang"
import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url"
import { parseSelectRateSearchParams, searchParamsToRecord } from "@/utils/url"
import { getValidDates } from "../getValidDates"
import { getTracking } from "./tracking"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
export default function Tracking({
@@ -34,16 +33,13 @@ export default function Tracking({
}) {
const lang = useLang()
const params = useSearchParams()
const selectRateParams = convertSearchParamsToObj<SelectRateSearchParams>(
searchParamsToRecord(params)
)
const booking = parseSelectRateSearchParams(searchParamsToRecord(params))
const { fromDate, toDate } = getValidDates(
selectRateParams.fromDate,
selectRateParams.toDate
)
if (!booking) return null
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 departureDate = toDate.toDate()
@@ -12,16 +12,15 @@ import FnFNotAllowedAlert from "../FnFNotAllowedAlert/FnFNotAllowedAlert"
import AvailabilityError from "./AvailabilityError"
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 { SelectHotelParams } from "@/utils/url"
export default async function SelectRatePage({
lang,
booking,
}: {
lang: Lang
booking: SelectHotelParams<SelectRateSearchParams>
booking: SelectRateBooking
}) {
const searchDetails = await getHotelSearchDetails(booking)
if (!searchDetails?.hotel) {