diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/start_page/[uid]/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/start_page/[uid]/page.tsx index b8496a7bb..39c2e10e0 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/start_page/[uid]/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/(contentTypes)/start_page/[uid]/page.tsx @@ -3,19 +3,21 @@ import { notFound } from "next/navigation" import { env } from "@/env/server" import StartPage from "@/components/ContentType/StartPage" +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 { generateMetadata } from "@/utils/generateMetadata" export default async function StartPagePage( - props: PageArgs<{}, BookingWidgetSearchData> + props: PageArgs<{}, NextSearchParams> ) { const searchParams = await props.searchParams if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") { return notFound() } - return + const booking = parseBookingWidgetSearchParams(searchParams) + + return } diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/map/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/map/page.tsx index 22bd28fd2..531888573 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/map/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/map/page.tsx @@ -1,22 +1,23 @@ +import { notFound } from "next/navigation" import { Suspense } from "react" import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" import { MapContainer } from "@/components/MapContainer" -import { convertSearchParamsToObj } from "@/utils/url" +import { parseSelectHotelSearchParams } from "@/utils/url" import styles from "./page.module.css" -import type { AlternativeHotelsSearchParams } 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 SelectHotelMapPage( - props: PageArgs + props: PageArgs ) { const searchParams = await props.searchParams - const booking = - convertSearchParamsToObj(searchParams) + const booking = parseSelectHotelSearchParams(searchParams) + + if (!booking) return notFound() return (
diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/page.tsx index 38d210df6..298cf5e89 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/alternative-hotels/page.tsx @@ -13,23 +13,19 @@ import { getTracking } from "@/components/HotelReservation/SelectHotel/tracking" import TrackingSDK from "@/components/TrackingSDK" import { getIntl } from "@/i18n" import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" -import { convertSearchParamsToObj } from "@/utils/url" +import { parseSelectHotelSearchParams } from "@/utils/url" -import type { - AlternativeHotelsSearchParams, - SelectHotelSearchParams, -} from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { LangParams, PageArgs } from "@/types/params" +import type { LangParams, NextSearchParams, PageArgs } from "@/types/params" export default async function AlternativeHotelsPage( - 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, true) @@ -42,10 +38,10 @@ export default async function AlternativeHotelsPage( bookingCode, childrenInRoom, city, + cityName, hotel: isAlternativeFor, noOfRooms, redemption, - selectHotelParams, } = searchDetails if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) { @@ -59,16 +55,18 @@ export default async function AlternativeHotelsPage( // TODO: This needs to be refactored into its // own functions - 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 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( @@ -92,11 +90,11 @@ export default async function AlternativeHotelsPage( 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)/details/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx index 23f5bbc64..3feed3122 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx @@ -20,15 +20,14 @@ import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDeta import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert" import RoomProvider from "@/providers/Details/RoomProvider" import EnterDetailsProvider from "@/providers/EnterDetailsProvider" -import { convertSearchParamsToObj } from "@/utils/url" +import { parseDetailsSearchParams } from "@/utils/url" import styles from "./page.module.css" -import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { LangParams, PageArgs } from "@/types/params" +import type { LangParams, NextSearchParams, PageArgs } from "@/types/params" export default async function DetailsPage( - props: PageArgs + props: PageArgs ) { const searchParams = await props.searchParams const params = await props.params @@ -37,11 +36,12 @@ export default async function DetailsPage( const selectRoomParams = new URLSearchParams(searchParams) - const { errorCode, ...booking } = - convertSearchParamsToObj(searchParams) - if ("modifyRateIndex" in booking) { + const booking = parseDetailsSearchParams(searchParams) + + if (!booking) return notFound() + + if (selectRoomParams.has("modifyRateIndex")) { selectRoomParams.delete("modifyRateIndex") - delete booking.modifyRateIndex } if ( diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index e5908f347..a83533e80 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -1,24 +1,25 @@ import stringify from "json-stable-stringify-without-jsonify" +import { notFound } from "next/navigation" import { Suspense } from "react" import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" import { MapContainer } from "@/components/MapContainer" -import { convertSearchParamsToObj } from "@/utils/url" +import { parseSelectHotelSearchParams } from "@/utils/url" import styles from "./page.module.css" -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 SelectHotelMapPage( - props: PageArgs + props: PageArgs ) { const searchParams = await props.searchParams const suspenseKey = stringify(searchParams) - const booking = - convertSearchParamsToObj(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 } /**