(searchParams)
+ const booking = parseSelectHotelSearchParams(searchParams)
+
+ if (!booking) return notFound()
return (
diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx
index e564c1dc8..531a91c21 100644
--- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx
@@ -12,19 +12,19 @@ import { getHotels } from "@/components/HotelReservation/SelectHotel/helpers"
import { getTracking } from "@/components/HotelReservation/SelectHotel/tracking"
import TrackingSDK from "@/components/TrackingSDK"
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, PageArgs } from "@/types/params"
+import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export default async function SelectHotelPage(
- props: PageArgs
+ props: PageArgs
) {
const searchParams = await props.searchParams
const params = await props.params
- const booking =
- convertSearchParamsToObj(searchParams)
+ const booking = parseSelectHotelSearchParams(searchParams)
+
+ if (!booking) return notFound()
const searchDetails = await getHotelSearchDetails(booking)
@@ -35,9 +35,9 @@ export default async function SelectHotelPage(
bookingCode,
childrenInRoom,
city,
+ cityName,
noOfRooms,
redemption,
- selectHotelParams,
} = searchDetails
if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) {
@@ -49,16 +49,18 @@ export default async function SelectHotelPage(
}
}
- const hotels = await getHotels(
- selectHotelParams,
- null,
+ const hotels = await getHotels({
+ fromDate: booking.fromDate,
+ toDate: booking.toDate,
+ rooms: booking.rooms,
+ isAlternativeFor: null,
bookingCode,
city,
- !!redemption
- )
+ redemption: !!redemption,
+ })
- 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(
@@ -82,11 +84,11 @@ export default async function SelectHotelPage(
adultsInRoom,
childrenInRoom,
hotels?.length ?? 0,
- selectHotelParams.hotelId,
+ booking.hotelId,
noOfRooms,
hotels?.[0]?.hotel.address.country,
hotels?.[0]?.hotel.address.city,
- selectHotelParams.city,
+ cityName,
bookingCode,
isBookingCodeRateAvailable,
redemption,
diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx
index 1d8eef18e..a8a07aaba 100644
--- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx
@@ -3,10 +3,9 @@ import { notFound } from "next/navigation"
import { combineRegExps, rateTypeRegex, REDEMPTION } from "@/constants/booking"
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, PageArgs } from "@/types/params"
+import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
const singleRoomRateTypes = combineRegExps(
[rateTypeRegex.ARB, rateTypeRegex.VOUCHER],
@@ -14,11 +13,13 @@ const singleRoomRateTypes = combineRegExps(
)
export default async function SelectRatePage(
- props: PageArgs
+ props: PageArgs
) {
const params = await props.params
const searchParams = await props.searchParams
- const booking = convertSearchParamsToObj(searchParams)
+ const booking = parseSelectRateSearchParams(searchParams)
+
+ if (!booking) return notFound()
const isMultiRoom = booking.rooms.length > 1
const isRedemption = booking.searchType === REDEMPTION
diff --git a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/destination_city_page/[uid]/page.tsx b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/destination_city_page/[uid]/page.tsx
index 508b0c034..8bb56b9e6 100644
--- a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/destination_city_page/[uid]/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/destination_city_page/[uid]/page.tsx
@@ -2,12 +2,14 @@ import { env } from "@/env/server"
import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests"
import { BookingWidget } from "@/components/BookingWidget"
+import { parseBookingWidgetSearchParams } from "@/utils/url"
-import type { BookingWidgetSearchData } from "@/types/components/bookingWidget"
-import type { PageArgs } from "@/types/params"
+import type { NextSearchParams, PageArgs } from "@/types/params"
-export default async function BookingWidgetDestinationCityPage(props: PageArgs<{}, BookingWidgetSearchData>) {
- const searchParams = await props.searchParams;
+export default async function BookingWidgetDestinationCityPage(
+ props: PageArgs<{}, NextSearchParams>
+) {
+ const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
return null
}
@@ -20,5 +22,7 @@ export default async function BookingWidgetDestinationCityPage(props: PageArgs<{
city: pageData?.city.name ?? "",
}
- return
+ const booking = parseBookingWidgetSearchParams(bookingWidgetSearchParams)
+
+ return
}
diff --git a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/hotel_page/[uid]/page.tsx b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/hotel_page/[uid]/page.tsx
index efdde751a..e8ae3a0dc 100644
--- a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/hotel_page/[uid]/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/(contentTypes)/hotel_page/[uid]/page.tsx
@@ -3,20 +3,18 @@ import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
import { BookingWidget } from "@/components/BookingWidget"
import { getLang } from "@/i18n/serverContext"
+import { parseBookingWidgetSearchParams } from "@/utils/url"
-import type { BookingWidgetSearchData } from "@/types/components/bookingWidget"
-import type { PageArgs } from "@/types/params"
+import type { NextSearchParams, PageArgs } from "@/types/params"
export default async function BookingWidgetHotelPage(
- props: PageArgs<{}, BookingWidgetSearchData & { subpage?: string }>
+ props: PageArgs<{}, NextSearchParams & { subpage?: string }>
) {
const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
return null
}
- const { bookingCode, subpage } = searchParams
-
const hotelPageData = await getHotelPage()
const hotelData = await getHotel({
hotelId: hotelPageData?.hotel_page_id || "",
@@ -24,6 +22,7 @@ export default async function BookingWidgetHotelPage(
isCardOnlyPayment: false,
})
+ const subpage = searchParams.subpage
const isMeetingSubpage =
subpage && hotelData?.additionalData.meetingRooms.nameInUrl === subpage
@@ -32,10 +31,12 @@ export default async function BookingWidgetHotelPage(
}
const bookingWidgetSearchParams = {
- bookingCode: bookingCode ?? "",
+ bookingCode: searchParams.bookingCode ?? "",
hotel: hotelData?.hotel.id ?? "",
city: hotelData?.hotel.cityName ?? "",
}
- return
+ const booking = parseBookingWidgetSearchParams(bookingWidgetSearchParams)
+
+ return
}
diff --git a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx
index 8e2cc6038..235081a69 100644
--- a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx
@@ -1,12 +1,14 @@
import { BookingWidget } from "@/components/BookingWidget"
+import { parseBookingWidgetSearchParams } from "@/utils/url"
-import type { BookingWidgetSearchData } from "@/types/components/bookingWidget"
-import type { LangParams, PageArgs } from "@/types/params"
+import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export default async function BookingWidgetPage(
- props: PageArgs
+ props: PageArgs
) {
const searchParams = await props.searchParams
- return
+ const booking = parseBookingWidgetSearchParams(searchParams)
+
+ return
}
diff --git a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/page.tsx b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/page.tsx
index 3cc112ee9..bb8490c22 100644
--- a/apps/scandic-web/app/[lang]/(live)/@bookingwidget/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/@bookingwidget/page.tsx
@@ -1,16 +1,20 @@
import { env } from "@/env/server"
import { BookingWidget } from "@/components/BookingWidget"
+import { parseBookingWidgetSearchParams } from "@/utils/url"
-import type { BookingWidgetSearchData } from "@/types/components/bookingWidget"
-import type { LangParams, PageArgs } from "@/types/params"
+import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
-export default async function BookingWidgetPage(props: PageArgs) {
- const params = await props.params;
- const searchParams = await props.searchParams;
+export default async function BookingWidgetPage(
+ props: PageArgs
+) {
+ const params = await props.params
+ const searchParams = await props.searchParams
if (!env.isLangLive(params.lang)) {
return null
}
- return
+ const booking = parseBookingWidgetSearchParams(searchParams)
+
+ return
}
diff --git a/apps/scandic-web/components/BookingWidget/Client.tsx b/apps/scandic-web/components/BookingWidget/Client.tsx
index de0923a3b..a5b073755 100644
--- a/apps/scandic-web/components/BookingWidget/Client.tsx
+++ b/apps/scandic-web/components/BookingWidget/Client.tsx
@@ -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(
- 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,
diff --git a/apps/scandic-web/components/BookingWidget/FloatingBookingWidget/index.tsx b/apps/scandic-web/components/BookingWidget/FloatingBookingWidget/index.tsx
index 0e13c2bba..279a7943d 100644
--- a/apps/scandic-web/components/BookingWidget/FloatingBookingWidget/index.tsx
+++ b/apps/scandic-web/components/BookingWidget/FloatingBookingWidget/index.tsx
@@ -8,7 +8,7 @@ import { FloatingBookingWidgetClient } from "./FloatingBookingWidgetClient"
import type { BookingWidgetProps } from "@/types/components/bookingWidget"
export async function FloatingBookingWidget({
- bookingWidgetSearchParams,
+ booking,
}: Omit) {
const isHidden = await isBookingWidgetHidden()
@@ -17,13 +17,13 @@ export async function FloatingBookingWidget({
}
let pageSettingsBookingCodePromise: Promise | null = null
- if (!bookingWidgetSearchParams.bookingCode) {
+ if (!booking.bookingCode) {
pageSettingsBookingCodePromise = getPageSettingsBookingCode()
}
return (
)
diff --git a/apps/scandic-web/components/BookingWidget/index.tsx b/apps/scandic-web/components/BookingWidget/index.tsx
index f817e884b..fe332626d 100644
--- a/apps/scandic-web/components/BookingWidget/index.tsx
+++ b/apps/scandic-web/components/BookingWidget/index.tsx
@@ -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 | null = null
- if (!bookingWidgetSearchParams.bookingCode) {
+ if (!booking.bookingCode) {
pageSettingsBookingCodePromise = getPageSettingsBookingCode()
}
return (
)
diff --git a/apps/scandic-web/components/ContentType/StartPage/index.tsx b/apps/scandic-web/components/ContentType/StartPage/index.tsx
index ceed14c37..335141b56 100644
--- a/apps/scandic-web/components/ContentType/StartPage/index.tsx
+++ b/apps/scandic-web/components/ContentType/StartPage/index.tsx
@@ -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({
{header.heading}
-
+
{header.hero_image ? (
+ booking: DetailsBooking
hotel: Hotel
rooms: Room[]
isMember: boolean
diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Tracking/tracking.ts b/apps/scandic-web/components/HotelReservation/EnterDetails/Tracking/tracking.ts
index 4d525026a..edb1451d4 100644
--- a/apps/scandic-web/components/HotelReservation/EnterDetails/Tracking/tracking.ts
+++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Tracking/tracking.ts
@@ -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,
+ booking: DetailsBooking,
hotel: Hotel,
rooms: Room[],
isMember: boolean,
diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx
index 964f65980..2a9104964 100644
--- a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx
@@ -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,
diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/tracking.ts b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/tracking.ts
index e4a83037a..8f09bfbf2 100644
--- a/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/tracking.ts
+++ b/apps/scandic-web/components/HotelReservation/SelectHotel/SelectHotelMap/tracking.ts
@@ -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,
diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts b/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts
index 75f017abb..c96bcd48e 100644
--- a/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts
+++ b/apps/scandic-web/components/HotelReservation/SelectHotel/helpers.ts
@@ -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,
})
)
)
diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/tracking.ts b/apps/scandic-web/components/HotelReservation/SelectHotel/tracking.ts
index 3ba88ae33..b62e59953 100644
--- a/apps/scandic-web/components/HotelReservation/SelectHotel/tracking.ts
+++ b/apps/scandic-web/components/HotelReservation/SelectHotel/tracking.ts
@@ -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,
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx
index e4beaff51..53601bad7 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/Summary.tsx
@@ -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 (
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/mapToPrice.ts
index 65fa2329c..81b8bc594 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/mapToPrice.ts
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/mapToPrice.ts
@@ -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,
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx
index beca783d7..4264d66cd 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx
@@ -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(
+ const booking = parseSelectRateSearchParams(
searchParamsToRecord(searchParams)
)
+ if (!booking) return notFound()
+
const bookingInput = selectRateRoomsAvailabilityInputSchema.safeParse({
booking,
lang,
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/Tracking/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/Tracking/index.tsx
index ecdc69776..f7b8cd613 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/Tracking/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/Tracking/index.tsx
@@ -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(
- 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()
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx
index d594f8509..92ed1d949 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx
@@ -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
+ booking: SelectRateBooking
}) {
const searchDetails = await getHotelSearchDetails(booking)
if (!searchDetails?.hotel) {
diff --git a/apps/scandic-web/constants/booking.ts b/apps/scandic-web/constants/booking.ts
index 4d125050d..c50f7aa79 100644
--- a/apps/scandic-web/constants/booking.ts
+++ b/apps/scandic-web/constants/booking.ts
@@ -9,8 +9,7 @@ import BedTwinIcon from "@scandic-hotels/design-system/Icons/BedTwinIcon"
import BedWallExtraIcon from "@scandic-hotels/design-system/Icons/BedWallExtraIcon"
import type { IconProps } from "@scandic-hotels/design-system/Icons"
-
-import type { JSX } from "react";
+import type { JSX } from "react"
export enum BookingStatusEnum {
BookingCompleted = "BookingCompleted",
@@ -39,6 +38,7 @@ export enum ChildBedTypeEnum {
export const FamilyAndFriendsCodes = ["D000029555", "D000029271", "D000029195"]
export const REDEMPTION = "redemption"
+export const bookingSearchTypes = [REDEMPTION] as const
export const SEARCHTYPE = "searchtype"
export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError"
diff --git a/apps/scandic-web/stores/enter-details/helpers.ts b/apps/scandic-web/stores/enter-details/helpers.ts
index 84e7e60b7..cbfb33440 100644
--- a/apps/scandic-web/stores/enter-details/helpers.ts
+++ b/apps/scandic-web/stores/enter-details/helpers.ts
@@ -9,7 +9,7 @@ import { detailsStorageName } from "."
import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
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 type { Package } from "@/types/requests/packages"
import type { PersistedState, RoomState } from "@/types/stores/enter-details"
@@ -28,8 +28,8 @@ export function extractGuestFromUser(user: NonNullable) {
}
export function checkIsSameBooking(
- prev: SelectRateSearchParams & { errorCode?: string },
- next: SelectRateSearchParams & { errorCode?: string }
+ prev: SelectRateBooking & { errorCode?: string },
+ next: SelectRateBooking & { errorCode?: string }
) {
const { rooms: prevRooms, errorCode: prevErrorCode, ...prevBooking } = prev
diff --git a/apps/scandic-web/stores/select-rate/helpers.ts b/apps/scandic-web/stores/select-rate/helpers.ts
index 78e94efb5..2f8f0959a 100644
--- a/apps/scandic-web/stores/select-rate/helpers.ts
+++ b/apps/scandic-web/stores/select-rate/helpers.ts
@@ -45,10 +45,14 @@ export function findProduct(
}
export function findProductInRoom(
- rateCode: string,
+ rateCode: string | undefined,
room: RoomConfiguration,
counterRateCode = ""
) {
+ if (!rateCode) {
+ return null
+ }
+
if (room.campaign.length) {
const campaignProduct = room.campaign.find((product) =>
findProduct(rateCode, product, counterRateCode)
@@ -84,14 +88,19 @@ export function findProductInRoom(
}
export function findSelectedRate(
- rateCode: string,
- counterRateCode: string,
- roomTypeCode: string,
+ rateCode: string | undefined,
+ counterRateCode: string | undefined,
+ roomTypeCode: string | undefined,
rooms: RoomConfiguration[] | AvailabilityError
) {
if (!Array.isArray(rooms)) {
return null
}
+
+ if (!rateCode) {
+ return null
+ }
+
return rooms.find((room) => {
if (room.roomTypeCode !== roomTypeCode) {
return false
diff --git a/apps/scandic-web/stores/tracking.ts b/apps/scandic-web/stores/tracking.ts
index baf08b081..a2e3c88f1 100644
--- a/apps/scandic-web/stores/tracking.ts
+++ b/apps/scandic-web/stores/tracking.ts
@@ -2,14 +2,12 @@
import { create } from "zustand"
-import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url"
+import { parseSelectRateSearchParams, searchParamsToRecord } from "@/utils/url"
import { checkIsSameBooking } from "./enter-details/helpers"
import type { ReadonlyURLSearchParams } from "next/navigation"
-import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
-
interface TrackingStoreState {
initialStartTime: number
setInitialPageLoadTime: (time: number) => void
@@ -81,14 +79,15 @@ const useTrackingStore = create((set, get) => ({
if (!currentPath?.match(/^\/(da|de|en|fi|no|sv)\/(hotelreservation)/))
return false
- const previousParamsObject =
- convertSearchParamsToObj(
- searchParamsToRecord(previousParams)
- )
- const currentParamsObject =
- convertSearchParamsToObj(
- searchParamsToRecord(currentParams)
- )
+ const previousParamsObject = parseSelectRateSearchParams(
+ searchParamsToRecord(previousParams)
+ )
+ const currentParamsObject = parseSelectRateSearchParams(
+ searchParamsToRecord(currentParams)
+ )
+
+ if (!previousParamsObject && !currentParamsObject) return false
+ if (!previousParamsObject || !currentParamsObject) return true
const isSameBooking = checkIsSameBooking(
previousParamsObject,
diff --git a/apps/scandic-web/types/components/bookingWidget/index.ts b/apps/scandic-web/types/components/bookingWidget/index.ts
index 37eb32e6e..6960842ac 100644
--- a/apps/scandic-web/types/components/bookingWidget/index.ts
+++ b/apps/scandic-web/types/components/bookingWidget/index.ts
@@ -1,12 +1,12 @@
import type { VariantProps } from "class-variance-authority"
import type { z } from "zod"
-import type { SearchParams } from "@/types/params"
import type {
bookingCodeSchema,
bookingWidgetSchema,
} from "@/components/Forms/BookingWidget/schema"
import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants"
+import type { BookingSearchType } from "../hotelReservation/booking"
import type { GuestsRoom } from "./guestsRoomsPicker"
export type BookingWidgetSchema = z.output
@@ -14,12 +14,12 @@ export type BookingCodeSchema = z.output
export type BookingWidgetSearchData = {
city?: string
- hotel?: string
+ hotelId?: string
fromDate?: string
toDate?: string
rooms?: GuestsRoom[]
bookingCode?: string
- searchType?: "redemption"
+ searchType?: BookingSearchType
}
export type BookingWidgetType = VariantProps<
@@ -28,16 +28,12 @@ export type BookingWidgetType = VariantProps<
export interface BookingWidgetProps {
type?: BookingWidgetType
- bookingWidgetSearchParams: Awaited<
- SearchParams["searchParams"]
- >
+ booking: BookingWidgetSearchData
}
export interface BookingWidgetClientProps {
type?: BookingWidgetType
- bookingWidgetSearchParams: Awaited<
- SearchParams["searchParams"]
- >
+ data: BookingWidgetSearchData
pageSettingsBookingCodePromise: Promise | null
}
diff --git a/apps/scandic-web/types/components/hotelReservation/booking.ts b/apps/scandic-web/types/components/hotelReservation/booking.ts
new file mode 100644
index 000000000..94b495e30
--- /dev/null
+++ b/apps/scandic-web/types/components/hotelReservation/booking.ts
@@ -0,0 +1,3 @@
+import type { bookingSearchTypes } from "@/constants/booking"
+
+export type BookingSearchType = (typeof bookingSearchTypes)[number]
diff --git a/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts b/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts
index d32b26665..88ce100d5 100644
--- a/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts
+++ b/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts
@@ -1,5 +1,6 @@
import type { z } from "zod"
+import type { PackageEnum } from "@/types/requests/packages"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
import type { SafeUser } from "@/types/user"
import type { getMultiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema"
@@ -8,7 +9,9 @@ import type {
signedInDetailsSchema,
} from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
import type { productTypePointsSchema } from "@/server/routers/hotels/schemas/productTypePrice"
+import type { BookingSearchType } from "../booking"
import type { Price } from "../price"
+import type { Child } from "../selectRate/selectRate"
export type DetailsSchema = z.output
export type MultiroomDetailsSchema = z.output<
@@ -31,3 +34,21 @@ export type JoinScandicFriendsCardProps = {
}
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[]
+ }[]
+}
diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts
index dfd841560..de1ea0e73 100644
--- a/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts
+++ b/apps/scandic-web/types/components/hotelReservation/selectHotel/map.ts
@@ -5,12 +5,8 @@ import type { Amenities } from "@/types/hotel"
import type { ProductTypeCheque } from "@/types/trpc/routers/hotel/availability"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
import type { imageSchema } from "@/server/routers/hotels/schemas/image"
-import type { SelectHotelParams } from "@/utils/url"
import type { CategorizedFilters } from "./hotelFilters"
-import type {
- AlternativeHotelsSearchParams,
- SelectHotelSearchParams,
-} from "./selectHotelSearchParams"
+import type { SelectHotelBooking } from "./selectHotel"
export interface HotelListingProps {
hotels: HotelResponse[]
@@ -76,8 +72,6 @@ export interface HotelCardDialogListingProps {
}
export type SelectHotelMapContainerProps = {
- booking: SelectHotelParams<
- SelectHotelSearchParams | AlternativeHotelsSearchParams
- >
+ booking: SelectHotelBooking
isAlternativeHotels?: boolean
}
diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/selectHotel.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/selectHotel.ts
index 9c05b4b25..3c89285ab 100644
--- a/apps/scandic-web/types/components/hotelReservation/selectHotel/selectHotel.ts
+++ b/apps/scandic-web/types/components/hotelReservation/selectHotel/selectHotel.ts
@@ -1,8 +1,7 @@
import type { Hotel } from "@/types/hotel"
-import type { Lang } from "@/constants/languages"
-import type { SelectHotelParams } from "@/utils/url"
+import type { BookingSearchType } from "../booking"
+import type { Child } from "../selectRate/selectRate"
import type { SidePeekEnum } from "../sidePeek"
-import type { AlternativeHotelsSearchParams } from "./selectHotelSearchParams"
export enum AvailabilityEnum {
Available = "Available",
@@ -20,8 +19,15 @@ export interface ContactProps {
hotel: Hotel
}
-export interface SelectHotelProps {
- booking: SelectHotelParams
- lang: Lang
- isAlternativeHotels?: boolean
+export type SelectHotelBooking = {
+ hotelId?: string
+ city?: string
+ fromDate: string
+ toDate: string
+ rooms: {
+ adults: number
+ childrenInRoom?: Child[]
+ }[]
+ bookingCode?: string
+ searchType?: BookingSearchType
}
diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/selectHotelSearchParams.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/selectHotelSearchParams.ts
deleted file mode 100644
index 0517d4cdb..000000000
--- a/apps/scandic-web/types/components/hotelReservation/selectHotel/selectHotelSearchParams.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { Room } from "../selectRate/selectRate"
-
-export interface SelectHotelSearchParams {
- city: string
- fromDate: string
- toDate: string
- rooms: Pick[]
- bookingCode: string
- searchType?: "redemption"
-}
-
-export interface AlternativeHotelsSearchParams {
- hotel: string
- fromDate: string
- toDate: string
- rooms: Pick[]
- bookingCode: string
- searchType?: "redemption"
-}
diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts
index 93369f85e..e9cc27336 100644
--- a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts
+++ b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts
@@ -5,6 +5,7 @@ import type {
RoomConfiguration,
} from "@/types/trpc/routers/hotel/roomAvailability"
import type { ChildBedMapEnum } from "../../bookingWidget/enums"
+import type { BookingSearchType } from "../booking"
export interface Child {
bed: ChildBedMapEnum
@@ -15,21 +16,20 @@ export interface Room {
adults: number
bookingCode?: string
childrenInRoom?: Child[]
- counterRateCode: string
+ counterRateCode?: string
packages?: PackageEnum[]
- rateCode: string
- roomTypeCode: string
+ rateCode?: string
+ roomTypeCode?: string
}
-export interface SelectRateSearchParams {
+export type SelectRateBooking = {
bookingCode?: string
city?: string
- errorCode?: string
fromDate: string
hotelId: string
rooms: Room[]
+ searchType?: BookingSearchType
toDate: string
- searchType?: "redemption"
}
export type Rate = {
diff --git a/apps/scandic-web/types/components/hotelReservation/summary.ts b/apps/scandic-web/types/components/hotelReservation/summary.ts
index d6822f45d..cd086c332 100644
--- a/apps/scandic-web/types/components/hotelReservation/summary.ts
+++ b/apps/scandic-web/types/components/hotelReservation/summary.ts
@@ -1,9 +1,13 @@
import type { CurrencyEnum } from "@/types/enums/currency"
import type { Packages } from "@/types/requests/packages"
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 { Child, SelectRateSearchParams } from "./selectRate/selectRate"
+import type { Child, SelectRateBooking } from "./selectRate/selectRate"
export type RoomsData = {
rateDetails: string[] | undefined
@@ -20,7 +24,7 @@ export interface SummaryProps {
}
export interface EnterDetailsSummaryProps {
- booking: SelectRateSearchParams
+ booking: DetailsBooking
isMember: boolean
totalPrice: Price
vat: number
@@ -30,7 +34,7 @@ export interface EnterDetailsSummaryProps {
}
export interface SelectRateSummaryProps {
- booking: SelectRateSearchParams
+ booking: SelectRateBooking
isMember: boolean
totalPrice: Price
vat: number
diff --git a/apps/scandic-web/types/params.ts b/apps/scandic-web/types/params.ts
index 841f722ff..90e17a61d 100644
--- a/apps/scandic-web/types/params.ts
+++ b/apps/scandic-web/types/params.ts
@@ -1,6 +1,8 @@
import type { Lang } from "@/constants/languages"
import type { PageContentTypeEnum } from "./requests/contentType"
+export type NextSearchParams = { [key: string]: string | string[] | undefined }
+
export type SearchParams = {
searchParams: Promise
}
diff --git a/apps/scandic-web/types/providers/enter-details.ts b/apps/scandic-web/types/providers/enter-details.ts
index 74806fd6a..9c47e0de4 100644
--- a/apps/scandic-web/types/providers/enter-details.ts
+++ b/apps/scandic-web/types/providers/enter-details.ts
@@ -1,10 +1,10 @@
import type { Room } from "@/types/providers/details/room"
import type { SafeUser } from "@/types/user"
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 {
- booking: SelectRateSearchParams
+ booking: DetailsBooking
breakfastPackages: BreakfastPackages
rooms: Room[]
searchParamsStr: string
diff --git a/apps/scandic-web/types/providers/rates.ts b/apps/scandic-web/types/providers/rates.ts
index 50381ec09..59f967a8b 100644
--- a/apps/scandic-web/types/providers/rates.ts
+++ b/apps/scandic-web/types/providers/rates.ts
@@ -1,10 +1,10 @@
import type { Room } from "@/types/hotel"
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"
export interface RatesProviderProps extends React.PropsWithChildren {
- booking: SelectRateSearchParams
+ booking: SelectRateBooking
hotelType: string | undefined
roomCategories: Room[]
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined
diff --git a/apps/scandic-web/types/stores/enter-details.ts b/apps/scandic-web/types/stores/enter-details.ts
index c4ee6dc60..03adbf1d6 100644
--- a/apps/scandic-web/types/stores/enter-details.ts
+++ b/apps/scandic-web/types/stores/enter-details.ts
@@ -7,19 +7,17 @@ import type {
BedTypeSelection,
} from "@/types/components/hotelReservation/enterDetails/bedType"
import type {
+ DetailsBooking,
DetailsSchema,
MultiroomDetailsSchema,
RoomPrice,
RoomRate,
SignedInDetailsSchema,
} 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 { Price } from "../components/hotelReservation/price"
-import type {
- Child,
- SelectRateSearchParams,
-} from "../components/hotelReservation/selectRate/selectRate"
+import type { CurrencyEnum } from "../enums/currency"
import type { Packages } from "../requests/packages"
export interface InitialRoomData {
@@ -78,7 +76,7 @@ export interface RoomState {
}
export type InitialState = {
- booking: SelectRateSearchParams
+ booking: DetailsBooking
rooms: InitialRoomData[]
vat: number
}
@@ -92,7 +90,7 @@ export interface DetailsState {
addPreSubmitCallback: (name: string, callback: () => void) => void
}
availableBeds: Record
- booking: SelectRateSearchParams
+ booking: DetailsBooking
breakfastPackages: BreakfastPackages
canProceedToPayment: boolean
isSubmitting: boolean
@@ -107,6 +105,6 @@ export interface DetailsState {
}
export type PersistedState = {
- booking: SelectRateSearchParams
+ booking: DetailsBooking
rooms: RoomState[]
}
diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts
index c51160932..3c2c84310 100644
--- a/apps/scandic-web/types/stores/rates.ts
+++ b/apps/scandic-web/types/stores/rates.ts
@@ -2,7 +2,7 @@ import type { DefaultFilterOptions } from "@/types/components/hotelReservation/s
import type {
Rate,
Room as RoomBooking,
- SelectRateSearchParams,
+ SelectRateBooking,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import type { CurrencyEnum } from "@/types/enums/currency"
import type { Room } from "@/types/hotel"
@@ -54,7 +54,7 @@ export interface SelectedRoom {
export interface RatesState {
activeRoom: number
- booking: SelectRateSearchParams
+ booking: SelectRateBooking
hotelType: string | undefined
isRedemptionBooking: boolean
packageOptions: DefaultFilterOptions[]
diff --git a/apps/scandic-web/utils/hotelSearchDetails.ts b/apps/scandic-web/utils/hotelSearchDetails.ts
index b15b36253..020eac72e 100644
--- a/apps/scandic-web/utils/hotelSearchDetails.ts
+++ b/apps/scandic-web/utils/hotelSearchDetails.ts
@@ -5,16 +5,9 @@ import { getLocations } from "@/lib/trpc/memoizedRequests"
import { generateChildrenString } from "@/components/HotelReservation/utils"
import { safeTry } from "@/utils/safeTry"
-import { type SelectHotelParams } from "@/utils/url"
-import type {
- AlternativeHotelsSearchParams,
- SelectHotelSearchParams,
-} from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
-import type {
- Child,
- SelectRateSearchParams,
-} from "@/types/components/hotelReservation/selectRate/selectRate"
+import type { BookingSearchType } from "@/types/components/hotelReservation/booking"
+import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import {
type HotelLocation,
isHotelLocation,
@@ -24,41 +17,44 @@ import {
export type ChildrenInRoom = (Child[] | null)[] | null
export type ChildrenInRoomString = (string | null)[] | null
-interface HotelSearchDetails {
+interface HotelSearchDetails {
adultsInRoom: number[]
bookingCode?: string
childrenInRoom: ChildrenInRoom
childrenInRoomString: ChildrenInRoomString
city: Location | null
+ cityName: string | undefined
hotel: HotelLocation | null
noOfRooms: number
redemption?: boolean
- selectHotelParams: SelectHotelParams & { city: string | undefined }
}
-export async function getHotelSearchDetails<
- T extends
- | SelectHotelSearchParams
- | SelectRateSearchParams
- | AlternativeHotelsSearchParams,
->(
- selectHotelParams: SelectHotelParams,
+export async function getHotelSearchDetails(
+ params: {
+ hotelId?: string
+ city?: string
+ rooms?: {
+ adults: number
+ childrenInRoom?: Child[]
+ }[]
+ bookingCode?: string
+ searchType?: BookingSearchType
+ },
isAlternativeHotels?: boolean
-): Promise | null> {
+): Promise {
const [locations, error] = await safeTry(getLocations())
if (!locations || error) {
return null
}
- const hotel =
- ("hotelId" in selectHotelParams &&
- (locations.find(
+ const hotel = params.hotelId
+ ? ((locations.find(
(location) =>
isHotelLocation(location) &&
"operaId" in location &&
- location.operaId === selectHotelParams.hotelId
- ) as HotelLocation | undefined)) ||
- null
+ location.operaId === params.hotelId
+ ) as HotelLocation | undefined) ?? null)
+ : null
if (isAlternativeHotels && !hotel) {
return notFound()
@@ -66,16 +62,13 @@ export async function getHotelSearchDetails<
const cityName = isAlternativeHotels
? hotel?.relationships.city.name
- : "city" in selectHotelParams
- ? (selectHotelParams.city as string | undefined)
- : undefined
+ : params.city
- const city =
- (typeof cityName === "string" &&
- locations.find(
+ const city = cityName
+ ? (locations.find(
(location) => location.name.toLowerCase() === cityName.toLowerCase()
- )) ||
- null
+ ) ?? null)
+ : null
if (!city && !hotel) return notFound()
if (isAlternativeHotels && (!city || !hotel)) return notFound()
@@ -84,7 +77,7 @@ export async function getHotelSearchDetails<
let childrenInRoom: ChildrenInRoom = null
let childrenInRoomString: ChildrenInRoomString = null
- const { rooms } = selectHotelParams
+ const { rooms } = params
if (rooms?.length) {
adultsInRoom = rooms.map((room) => room.adults ?? 0)
@@ -97,13 +90,13 @@ export async function getHotelSearchDetails<
return {
adultsInRoom,
- bookingCode: selectHotelParams.bookingCode ?? undefined,
+ bookingCode: params.bookingCode ?? undefined,
childrenInRoom,
childrenInRoomString,
city,
+ cityName,
hotel,
noOfRooms: rooms?.length ?? 0,
- redemption: selectHotelParams.searchType === REDEMPTION,
- selectHotelParams: { city: cityName, ...selectHotelParams },
+ redemption: params.searchType === REDEMPTION,
}
}
diff --git a/apps/scandic-web/utils/searchParams.test.ts b/apps/scandic-web/utils/searchParams.test.ts
new file mode 100644
index 000000000..6a304edd0
--- /dev/null
+++ b/apps/scandic-web/utils/searchParams.test.ts
@@ -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 = {}
+ for (const [key, value] of searchParams.entries()) {
+ obj[key] = value
+ }
+ return obj
+}
diff --git a/apps/scandic-web/utils/searchParams.ts b/apps/scandic-web/utils/searchParams.ts
new file mode 100644
index 000000000..103e0d364
--- /dev/null
+++ b/apps/scandic-web/utils/searchParams.ts
@@ -0,0 +1,209 @@
+import type { z } from "zod"
+
+import type { NextSearchParams } from "@/types/params"
+
+type ParseOptions = {
+ keyRenameMap?: Record
+ typeHints?: Record
+ schema?: z.ZodObject
+}
+
+type ParseOptionsWithSchema = ParseOptions & {
+ schema: z.ZodObject
+}
+
+// This ensures that the return type is correct when a schema is provided
+export function parseSearchParams(
+ searchParams: NextSearchParams,
+ options: ParseOptionsWithSchema
+): z.infer
+export function parseSearchParams(
+ searchParams: NextSearchParams,
+ options?: ParseOptions
+): Record
+
+/**
+ * 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(
+ searchParams: NextSearchParams,
+ options?: ParseOptions
+) {
+ const entries = Object.entries(searchParams)
+
+ const buildObject = getBuilder(options || {})
+
+ const resultObject: Record = {}
+ 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(options: ParseOptions) {
+ const keyRenameMap = options.keyRenameMap || {}
+ const typeHints = options.typeHints || {}
+
+ return function buildNestedObject(
+ obj: Record,
+ 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
+ typeHints?: Record
+ 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,
+ 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 {
+ return typeof value === "object" && value !== null
+}
diff --git a/apps/scandic-web/utils/url.ts b/apps/scandic-web/utils/url.ts
index 12edf4d81..1e8f40245 100644
--- a/apps/scandic-web/utils/url.ts
+++ b/apps/scandic-web/utils/url.ts
@@ -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[] }
-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 = Omit & {
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(
- searchParams: Record
-): SelectHotelParams {
- const searchParamsObject = Object.entries(searchParams).reduce<
- SelectHotelParams
- >((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)
-
- return searchParamsObject
+ return result
+ } catch (error) {
+ console.log("[URL] Error parsing search params for booking widget:", error)
+ return {}
+ }
}
-export function convertObjToSearchParams(
- 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
}
/**