Merged in feat/sw-2873-move-selecthotel-to-booking-flow (pull request #2727)

feat(SW-2873): Move select-hotel to booking flow

* crude setup of select-hotel in partner-sas

* wip

* Fix linting

* restructure tracking files

* Remove dependency on trpc in tracking hooks

* Move pageview tracking to common

* Fix some lint and import issues

* Add AlternativeHotelsPage

* Add SelectHotelMapPage

* Add AlternativeHotelsMapPage

* remove next dependency in tracking store

* Remove dependency on react in tracking hooks

* move isSameBooking to booking-flow

* Inject searchParamsComparator into tracking store

* Move useTrackHardNavigation to common

* Move useTrackSoftNavigation to common

* Add TrackingSDK to partner-sas

* call serverclient in layout

* Remove unused css

* Update types

* Move HotelPin type

* Fix todos

* Merge branch 'master' into feat/sw-2873-move-selecthotel-to-booking-flow

* Merge branch 'master' into feat/sw-2873-move-selecthotel-to-booking-flow

* Fix component


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-09-01 08:37:00 +00:00
parent 93a90bef9d
commit 87402a2092
157 changed files with 2026 additions and 1376 deletions

View File

@@ -1,4 +1,4 @@
import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton"
import { SelectHotelSkeleton } from "@scandic-hotels/booking-flow/components/SelectHotel"
export default function AlternativeHotelsLoading() {
return <SelectHotelSkeleton />

View File

@@ -1,35 +1,30 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage"
import { parseSelectHotelSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer"
import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton"
import { MapContainer } from "@/components/MapContainer"
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage(
export default async function AlternativeHotelsMapPage(
props: PageArgs<LangParams, NextSearchParams>
) {
const searchParams = await props.searchParams
const booking = parseSelectHotelSearchParams(searchParams)
if (!booking) return notFound()
const lang = await getLang()
return (
<div className={styles.main}>
<MapContainer>
<Suspense
key={searchParams.hotel}
fallback={<SelectHotelMapContainerSkeleton />}
>
<SelectHotelMapContainer booking={booking} isAlternativeHotels />
</Suspense>
</MapContainer>
<AlternativeHotelsMapPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
)}
/>
</div>
)
}

View File

@@ -1,20 +1,7 @@
import stringify from "json-stable-stringify-without-jsonify"
import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage"
import { parseSelectHotelSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { alternativeHotelsMap } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { FamilyAndFriendsCodes } from "@/constants/booking"
import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert"
import SelectHotel from "@/components/HotelReservation/SelectHotel"
import { getHotels } from "@/components/HotelReservation/SelectHotel/helpers"
import { getSelectHotelTracking } from "@/components/HotelReservation/SelectHotel/tracking"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { getLang } from "@/i18n/serverContext"
import {
type LangParams,
@@ -26,112 +13,18 @@ export default async function AlternativeHotelsPage(
props: PageArgs<LangParams, NextSearchParams>
) {
const searchParams = await props.searchParams
const params = await props.params
const lang = await getLang()
const booking = parseSelectHotelSearchParams(searchParams)
if (!booking) return notFound()
const searchDetails = await getHotelSearchDetails(booking, true)
if (!searchDetails || !searchDetails.hotel || !searchDetails.city) {
return notFound()
}
if (
booking.bookingCode &&
FamilyAndFriendsCodes.includes(booking.bookingCode)
) {
const cookieStore = await cookies()
const isInvalidFNF = cookieStore.get("sc")?.value !== "1"
if (isInvalidFNF) {
return <FnFNotAllowedAlert />
}
}
// TODO: This needs to be refactored into its
// own functions
const hotels = await getHotels({
fromDate: booking.fromDate,
toDate: booking.toDate,
rooms: booking.rooms,
isAlternativeFor: searchDetails.hotel,
bookingCode: booking.bookingCode,
city: searchDetails.city,
redemption: !!searchDetails.redemption,
})
const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(booking.toDate)
const isRedemptionAvailability = searchDetails.redemption
? hotels.some(
(hotel) => hotel.availability.productType?.redemptions?.length
)
: false
const isBookingCodeRateAvailable = booking.bookingCode
? hotels.some(
(hotel) =>
hotel.availability.bookingCode &&
hotel.availability.status === "Available"
)
: false
const { hotelsTrackingData, pageTrackingData } = getSelectHotelTracking({
lang: params.lang,
pageId: searchDetails.hotel ? "alternative-hotels" : "select-hotel",
pageName: searchDetails.hotel
? "hotelreservation|alternative-hotels"
: "hotelreservation|select-hotel",
siteSections: searchDetails.hotel
? "hotelreservation|alternative-hotels"
: "hotelreservation|select-hotel",
arrivalDate,
departureDate,
rooms: booking.rooms,
hotelsResult: hotels?.length ?? 0,
searchTerm: searchDetails.hotel
? booking.hotelId
: searchDetails.cityIdentifier,
country: hotels?.[0]?.hotel.address.country,
hotelCity: hotels?.[0]?.hotel.address.city,
bookingCode: booking.bookingCode,
isBookingCodeRateAvailable,
isRedemption: searchDetails.redemption,
isRedemptionAvailable: isRedemptionAvailability,
})
const mapHref = alternativeHotelsMap(params.lang)
const intl = await getIntl()
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: searchDetails.hotel.name,
}
)
const suspenseKey = stringify(searchParams)
return (
<>
<SelectHotel
bookingCode={booking.bookingCode}
city={searchDetails.city}
hotels={hotels}
isAlternative={!!searchDetails.hotel}
isBookingCodeRateAvailable={isBookingCodeRateAvailable}
mapHref={mapHref}
title={title}
/>
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
<AlternativeHotelsPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
pageData={pageTrackingData}
hotelInfo={hotelsTrackingData}
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
</Suspense>
</>
)}
/>
)
}

View File

@@ -2,9 +2,10 @@ import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import FnFNotAllowedAlert from "@scandic-hotels/booking-flow/components/FnFNotAllowedAlert"
import { parseDetailsSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { FamilyAndFriendsCodes } from "@scandic-hotels/common/constants/familyAndFriends"
import { FamilyAndFriendsCodes } from "@/constants/booking"
import {
getBreakfastPackages,
getHotel,
@@ -19,7 +20,6 @@ import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDetails/Tracking"
import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert"
import RoomProvider from "@/providers/Details/RoomProvider"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"

View File

@@ -1,11 +1,12 @@
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@scandic-hotels/common/tracking/types"
import TrackingSDK from "@/components/TrackingSDK"
import styles from "./page.module.css"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelReservationPage(

View File

@@ -1,4 +1,4 @@
import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton"
import { SelectHotelSkeleton } from "@scandic-hotels/booking-flow/components/SelectHotel"
export default function SelectHotelLoading() {
return <SelectHotelSkeleton />

View File

@@ -1,12 +1,7 @@
import stringify from "json-stable-stringify-without-jsonify"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage"
import { parseSelectHotelSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer"
import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton"
import { MapContainer } from "@/components/MapContainer"
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
@@ -16,22 +11,20 @@ export default async function SelectHotelMapPage(
props: PageArgs<LangParams, NextSearchParams>
) {
const searchParams = await props.searchParams
const suspenseKey = stringify(searchParams)
const booking = parseSelectHotelSearchParams(searchParams)
if (!booking) return notFound()
const lang = await getLang()
return (
<div className={styles.main}>
<MapContainer>
<Suspense
key={suspenseKey}
fallback={<SelectHotelMapContainerSkeleton />}
>
<SelectHotelMapContainer booking={booking} />
</Suspense>
</MapContainer>
<SelectHotelMapPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
)}
/>
</div>
)
}

View File

@@ -1,19 +1,7 @@
import stringify from "json-stable-stringify-without-jsonify"
import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
import { parseSelectHotelSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { selectHotelMap } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { FamilyAndFriendsCodes } from "@/constants/booking"
import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert"
import SelectHotel from "@/components/HotelReservation/SelectHotel"
import { getHotels } from "@/components/HotelReservation/SelectHotel/helpers"
import { getSelectHotelTracking } from "@/components/HotelReservation/SelectHotel/tracking"
import TrackingSDK from "@/components/TrackingSDK"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { getLang } from "@/i18n/serverContext"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
@@ -21,93 +9,18 @@ export default async function SelectHotelPage(
props: PageArgs<LangParams, NextSearchParams>
) {
const searchParams = await props.searchParams
const params = await props.params
const lang = await getLang()
const booking = parseSelectHotelSearchParams(searchParams)
if (!booking) return notFound()
const searchDetails = await getHotelSearchDetails(booking)
if (!searchDetails || !searchDetails.city) return notFound()
if (
booking.bookingCode &&
FamilyAndFriendsCodes.includes(booking.bookingCode)
) {
const cookieStore = await cookies()
const isInvalidFNF = cookieStore.get("sc")?.value !== "1"
if (isInvalidFNF) {
return <FnFNotAllowedAlert />
}
}
const { city, redemption } = searchDetails
const hotels = await getHotels({
fromDate: booking.fromDate,
toDate: booking.toDate,
rooms: booking.rooms,
isAlternativeFor: null,
bookingCode: booking.bookingCode,
city: city,
redemption: !!redemption,
})
const isRedemptionAvailability = redemption
? hotels.some(
(hotel) => hotel.availability.productType?.redemptions?.length
)
: false
const isBookingCodeRateAvailable = booking.bookingCode
? hotels.some(
(hotel) =>
hotel.availability.bookingCode &&
hotel.availability.status === "Available"
)
: false
const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(booking.toDate)
const { hotelsTrackingData, pageTrackingData } = getSelectHotelTracking({
rooms: booking.rooms,
lang: params.lang,
pageId: "select-hotel",
pageName: "hotelreservation|select-hotel",
siteSections: "hotelreservation|select-hotel",
arrivalDate,
departureDate,
hotelsResult: hotels?.length ?? 0,
searchTerm: booking.hotelId,
country: hotels?.[0]?.hotel.address.country,
hotelCity: hotels?.[0]?.hotel.address.city,
bookingCode: booking.bookingCode,
isBookingCodeRateAvailable,
isRedemption: redemption,
isRedemptionAvailable: isRedemptionAvailability,
})
const mapHref = selectHotelMap(params.lang)
const suspenseKey = stringify(searchParams)
return (
<>
<SelectHotel
bookingCode={booking.bookingCode}
isBookingCodeRateAvailable={isBookingCodeRateAvailable}
city={city}
hotels={hotels}
mapHref={mapHref}
title={city.name}
/>
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
<SelectHotelPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
pageData={pageTrackingData}
hotelInfo={hotelsTrackingData}
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
</Suspense>
</>
)}
/>
)
}

View File

@@ -1,10 +1,10 @@
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
} from "@scandic-hotels/common/tracking/types"
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
export default async function Tracking() {
const lang = await getLang()

View File

@@ -9,13 +9,13 @@ import Script from "next/script"
import { SessionProvider } from "next-auth/react"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { BookingFlowTrackingProvider } from "@scandic-hotels/booking-flow/BookingFlowTrackingProvider"
import { Lang } from "@scandic-hotels/common/constants/language"
import { ToastHandler } from "@scandic-hotels/design-system/ToastHandler"
import TrpcProvider from "@/lib/trpc/Provider"
import { SessionRefresher } from "@/components/Auth/TokenRefresher"
import { BookingFlowProviders } from "@/components/BookingFlowProviders"
import CookieBotConsent from "@/components/CookieBot"
import Footer from "@/components/Footer"
import Header from "@/components/Header"
@@ -30,8 +30,6 @@ import { FontPreload } from "@/fonts/font-preloading"
import { getMessages } from "@/i18n"
import ClientIntlProvider from "@/i18n/Provider"
import { setLang } from "@/i18n/serverContext"
import { trackAccordionClick, trackOpenSidePeekEvent } from "@/utils/tracking"
import { trackBookingSearchClick } from "@/utils/tracking/booking"
import type { LangParams, LayoutArgs } from "@/types/params"
@@ -72,13 +70,7 @@ export default async function RootLayout(
<NuqsAdapter>
<TrpcProvider>
<RACRouterProvider>
<BookingFlowTrackingProvider
trackingFunctions={{
trackBookingSearchClick,
trackAccordionItemOpen: trackAccordionClick,
trackOpenSidePeek: trackOpenSidePeekEvent,
}}
>
<BookingFlowProviders>
<RouteChange />
<SitewideAlert />
<Header />
@@ -91,7 +83,7 @@ export default async function RootLayout(
<CookieBotConsent />
<UserExists />
<ReactQueryDevtools initialIsOpen={false} />
</BookingFlowTrackingProvider>
</BookingFlowProviders>
</RACRouterProvider>
</TrpcProvider>
</NuqsAdapter>