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

@@ -1,10 +1,20 @@
import { z } from "zod"
import { bookingSearchTypes } from "@/constants/booking"
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 {
Child,
Room,
SelectRateBooking,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { NextSearchParams } from "@/types/params"
export function removeMultipleSlashes(pathname: string) {
return pathname.replaceAll(/\/\/+/g, "/")
@@ -20,151 +30,213 @@ export function removeTrailingSlash(pathname: string) {
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"> & {
hotelId: string
} & 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) {
return Object.fromEntries(searchParams.entries())
}
export function convertSearchParamsToObj<T extends PartialRoom>(
searchParams: Record<string, string>
): SelectHotelParams<T> {
const searchParamsObject = Object.entries(searchParams).reduce<
SelectHotelParams<T>
>((acc, [key, value]) => {
// The params are sometimes indexed with a number (for ex: `room[0].adults`),
// so we need to split them by . or []
const keys = key.replace(/\]/g, "").split(/\[|\./)
const firstKey = getKeyFromSearchParam(keys[0])
const keyRenameMap = {
room: "rooms",
ratecode: "rateCode",
counterratecode: "counterRateCode",
roomtype: "roomTypeCode",
fromdate: "fromDate",
todate: "toDate",
hotel: "hotelId",
child: "childrenInRoom",
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
if (firstKey === "rooms") {
// Rooms are always indexed with a number, so we need to extract the index
const index = Number(keys[1])
const roomObject =
acc.rooms && Array.isArray(acc.rooms) ? acc.rooms : (acc.rooms = [])
export function parseBookingWidgetSearchParams(
searchParams: NextSearchParams
): BookingWidgetSearchData {
try {
const result = parseSearchParams(searchParams, {
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
if (!roomObject[index]) {
roomObject[index] = {}
}
// 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
return result
} catch (error) {
console.log("[URL] Error parsing search params for booking widget:", error)
return {}
}
}
export function convertObjToSearchParams<T>(
bookingData: T & PartialRoom,
intitalSearchParams = {} as URLSearchParams
) {
const bookingSearchParams = new URLSearchParams(intitalSearchParams)
Object.entries(bookingData).forEach(([key, value]) => {
if (key === "rooms") {
value.forEach((item, index) => {
if (item?.adults) {
bookingSearchParams.set(
`room[${index}].adults`,
item.adults.toString()
)
}
if (item?.childrenInRoom) {
item.childrenInRoom.forEach((child, childIndex) => {
bookingSearchParams.set(
`room[${index}].child[${childIndex}].age`,
child.age.toString()
)
bookingSearchParams.set(
`room[${index}].child[${childIndex}].bed`,
child.bed.toString()
)
export function parseSelectHotelSearchParams(
searchParams: NextSearchParams
): SelectHotelBooking | null {
try {
const result = parseSearchParams(searchParams, {
keyRenameMap,
schema: z.object({
city: z.string(),
hotelId: z.string().optional(),
fromDate: z.string(),
toDate: z.string(),
bookingCode: z.string().optional(),
searchType: searchTypeSchema,
rooms: z.array(
z.object({
adults: adultsSchema,
childrenInRoom: z
.array(
z.object({
bed: childBedSchema,
age: childAgeSchema,
})
)
.optional(),
})
}
if (item?.roomTypeCode) {
bookingSearchParams.set(`room[${index}].roomtype`, item.roomTypeCode)
}
if (item?.rateCode) {
bookingSearchParams.set(`room[${index}].ratecode`, item.rateCode)
}
),
}),
})
if (item?.counterRateCode) {
bookingSearchParams.set(
`room[${index}].counterratecode`,
item.counterRateCode
)
}
return result
} catch (error) {
console.log("[URL] Error parsing search params for select hotel:", error)
if (item.packages && item.packages.length > 0) {
bookingSearchParams.set(
`room[${index}].packages`,
item.packages.join(",")
)
}
})
} else {
bookingSearchParams.set(getSearchParamFromKey(key), value.toString())
}
return null
}
}
export function parseSelectRateSearchParams(
searchParams: NextSearchParams
): SelectRateBooking | null {
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().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
}
/**