Files
web/apps/scandic-web/utils/url.ts
Anton Gunnarsson bff34b034e Merged in feat/sw-2857-refactor-booking-flow-url-updates (pull request #2302)
feat(SW-2857): Refactor booking flow url updates

* Add support for removing parameters when using initial values in serializeSearchParams

* Don't manually write search params in rate store

* Booking is already from live search params so no need

* Fix input type in serializeBookingSearchParams


Approved-by: Linus Flood
2025-06-09 09:16:22 +00:00

303 lines
8.0 KiB
TypeScript

import { z } from "zod"
import { bookingSearchTypes } from "@/constants/booking"
import { Lang } from "@/constants/languages"
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 {
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, "/")
}
export function removeTrailingSlash(pathname: string) {
if (pathname.endsWith("/")) {
// Remove the trailing slash
return pathname.slice(0, -1)
}
return pathname
}
type PartialRoom = { rooms?: Partial<Room>[] }
export type SelectHotelParams<T> = Omit<T, "hotel"> & {
hotelId: string
} & PartialRoom
export function searchParamsToRecord(searchParams: URLSearchParams) {
return Object.fromEntries(searchParams.entries())
}
const keyRenameMap = {
room: "rooms",
ratecode: "rateCode",
counterratecode: "counterRateCode",
roomtype: "roomTypeCode",
fromdate: "fromDate",
todate: "toDate",
hotel: "hotelId",
child: "childrenInRoom",
searchtype: "searchType",
}
const typeHints = {
filters: "COMMA_SEPARATED_ARRAY",
packages: "COMMA_SEPARATED_ARRAY",
} as const
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)
export function parseBookingWidgetSearchParams(
searchParams: NextSearchParams
): BookingWidgetSearchData {
try {
const result = parseSearchParams(searchParams, {
keyRenameMap,
typeHints,
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(),
}),
})
return result
} catch (error) {
console.log("[URL] Error parsing search params for booking widget:", error)
return {}
}
}
export function parseSelectHotelSearchParams(
searchParams: NextSearchParams
): SelectHotelBooking | null {
try {
const result = parseSearchParams(searchParams, {
keyRenameMap,
typeHints,
schema: z.object({
city: z.string().optional(),
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(),
})
),
}),
})
return result
} catch (error) {
console.log("[URL] Error parsing search params for select hotel:", error)
return null
}
}
export function parseSelectRateSearchParams(
searchParams: NextSearchParams
): SelectRateBooking | null {
try {
const result = parseSearchParams(searchParams, {
keyRenameMap,
typeHints,
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,
typeHints,
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:
| BookingWidgetSearchData
| SelectHotelBooking
| SelectRateBooking
| DetailsBooking,
{ initialSearchParams }: { initialSearchParams?: URLSearchParams } = {}
) {
return serializeSearchParams(obj, {
keyRenameMap: reversedKeyRenameMap,
initialSearchParams,
typeHints,
})
}
/**
* Returns the TLD (top-level domain) for a given language.
* @param lang - The language to get the TLD for
* @returns The TLD for the given language
*/
export function getTldForLanguage(lang: Lang): string {
switch (lang) {
case Lang.sv:
return "se"
case Lang.no:
return "no"
case Lang.da:
return "dk"
case Lang.fi:
return "fi"
case Lang.de:
return "de"
default:
return "com"
}
}
/**
* Constructs a URL with the correct TLD (top-level domain) based on lang, for current web.
* @param params - Object containing path, lang, and baseUrl
* @param params.path - The path to append to the URL
* @param params.lang - The language to use for TLD
* @param params.baseUrl - The base URL to use (e.g. https://www.scandichotels.com)
* @returns The complete URL with language-specific TLD
*/
export function getCurrentWebUrl({
path,
lang,
baseUrl = "https://www.scandichotels.com", // Fallback for ephemeral environments (e.g. deploy previews).
}: {
path: string
lang: Lang
baseUrl?: string
}): string {
const tld = getTldForLanguage(lang)
const url = new URL(path, baseUrl)
if (tld !== "com") {
url.host = url.host.replace(".com", `.${tld}`)
}
return url.toString()
}