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

@@ -0,0 +1,28 @@
import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage"
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import type { LangParams, PageArgs } from "@/types/params"
export default async function AlternativeHotelsMapPage(
props: PageArgs<LangParams>
) {
const searchParams = await props.searchParams
const lang = await getLang()
return (
<div>
<AlternativeHotelsMapPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
)}
/>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage"
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import { type LangParams, type PageArgs } from "@/types/params"
export default async function AlternativeHotelsPage(
props: PageArgs<LangParams>
) {
const searchParams = await props.searchParams
const lang = await getLang()
return (
<AlternativeHotelsPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
)}
/>
)
}

View File

@@ -0,0 +1,24 @@
import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage"
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams
const lang = await getLang()
return (
<SelectHotelMapPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
)}
/>
)
}

View File

@@ -1,4 +1,24 @@
export default async function SelectHotelPage() { import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
return <div>select-hotel</div> import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelPage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams
const lang = await getLang()
return (
<SelectHotelPagePrimitive
lang={lang}
searchParams={searchParams}
renderTracking={(props) => (
<TrackingSDK
hotelInfo={props.hotelsTrackingData}
pageData={props.pageTrackingData}
/>
)}
/>
)
} }

View File

@@ -4,10 +4,13 @@ import "@scandic-hotels/design-system/normalize.css"
import "@scandic-hotels/design-system/design-system-new-deprecated.css" import "@scandic-hotels/design-system/design-system-new-deprecated.css"
import "../../globals.css" import "../../globals.css"
import { BookingFlowContextProvider } from "@scandic-hotels/booking-flow/BookingFlowContextProvider"
import { BookingFlowTrackingProvider } from "@scandic-hotels/booking-flow/BookingFlowTrackingProvider" import { BookingFlowTrackingProvider } from "@scandic-hotels/booking-flow/BookingFlowTrackingProvider"
import { Lang } from "@scandic-hotels/common/constants/language" import { Lang } from "@scandic-hotels/common/constants/language"
import { TrpcProvider } from "@scandic-hotels/trpc/Provider" import { TrpcProvider } from "@scandic-hotels/trpc/Provider"
import { serverClient } from "@/lib/trpc"
import { getMessages } from "@/i18n" import { getMessages } from "@/i18n"
import ClientIntlProvider from "@/i18n/Provider" import ClientIntlProvider from "@/i18n/Provider"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
@@ -15,6 +18,7 @@ import { setLang } from "@/i18n/serverContext"
import { import {
trackAccordionItemOpen, trackAccordionItemOpen,
trackBookingSearchClick, trackBookingSearchClick,
trackGenericEvent,
trackOpenSidePeek, trackOpenSidePeek,
} from "../utils/tracking" } from "../utils/tracking"
@@ -43,6 +47,10 @@ export default async function RootLayout(props: RootLayoutProps) {
setLang(lang) setLang(lang)
const messages = await getMessages(lang) const messages = await getMessages(lang)
// TODO we need this import right now to ensure configureServerClient is called,
// but check where we do this
const _caller = await serverClient()
return ( return (
<html lang="en"> <html lang="en">
<head>{/* TODO */}</head> <head>{/* TODO */}</head>
@@ -55,28 +63,36 @@ export default async function RootLayout(props: RootLayoutProps) {
> >
{/* TODO handle onError */} {/* TODO handle onError */}
<TrpcProvider> <TrpcProvider>
<BookingFlowTrackingProvider <BookingFlowContextProvider
trackingFunctions={{ data={{
trackBookingSearchClick, // TODO
trackAccordionItemOpen, isLoggedIn: false,
trackOpenSidePeek,
}} }}
> >
<header <BookingFlowTrackingProvider
style={{ trackingFunctions={{
height: 64, trackBookingSearchClick,
backgroundColor: "dodgerblue", trackAccordionItemOpen,
color: "white", trackOpenSidePeek,
display: "flex", trackGenericEvent,
alignItems: "center",
justifyContent: "center",
}} }}
> >
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} <header
<h1>SAS</h1> style={{
</header> height: 64,
<main>{children}</main> backgroundColor: "dodgerblue",
</BookingFlowTrackingProvider> color: "white",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<h1>SAS</h1>
</header>
<main>{children}</main>
</BookingFlowTrackingProvider>
</BookingFlowContextProvider>
</TrpcProvider> </TrpcProvider>
</ClientIntlProvider> </ClientIntlProvider>
</div> </div>

View File

@@ -1,8 +1,6 @@
import { BookingWidget } from "@scandic-hotels/booking-flow/BookingWidget" import { BookingWidget } from "@scandic-hotels/booking-flow/BookingWidget"
import { parseBookingWidgetSearchParams } from "@scandic-hotels/booking-flow/utils/url" import { parseBookingWidgetSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { serverClient } from "@/lib/trpc"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Lang } from "@scandic-hotels/common/constants/language" import type { Lang } from "@scandic-hotels/common/constants/language"
@@ -14,9 +12,6 @@ type SearchParams<S = {}> = {
export default async function Home(props: SearchParams<{ lang: Lang }>) { export default async function Home(props: SearchParams<{ lang: Lang }>) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
// TODO we need this import right now to ensure configureServerClient is called,
// but we should ensure it's called in a layout instead.
const _caller = await serverClient()
const lang = await getLang() const lang = await getLang()
const booking = parseBookingWidgetSearchParams(searchParams) const booking = parseBookingWidgetSearchParams(searchParams)

View File

@@ -22,3 +22,7 @@ export function trackOpenSidePeek(input: {
}) { }) {
console.warn("TODO: Implement trackOpenSidePeek", { input }) console.warn("TODO: Implement trackOpenSidePeek", { input })
} }
export function trackGenericEvent(data: any) {
console.warn("TODO: Implement trackGenericEvent", { data })
}

View File

@@ -0,0 +1,55 @@
"use client"
import { usePathname } from "next/navigation"
import { useTrackHardNavigation } from "@scandic-hotels/common/tracking/useTrackHardNavigation"
import { useTrackSoftNavigation } from "@scandic-hotels/common/tracking/useTrackSoftNavigation"
import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "@/hooks/useLang"
import type {
TrackingSDKAncillaries,
TrackingSDKHotelInfo,
TrackingSDKPageData,
TrackingSDKPaymentInfo,
} from "@scandic-hotels/common/tracking/types"
export default function TrackingSDK({
pageData,
hotelInfo,
paymentInfo,
ancillaries,
}: {
pageData: TrackingSDKPageData
hotelInfo?: TrackingSDKHotelInfo
paymentInfo?: TrackingSDKPaymentInfo
ancillaries?: TrackingSDKAncillaries
}) {
const lang = useLang()
const pathName = usePathname()
const { data, isError } = trpc.user.userTrackingInfo.useQuery({
lang,
})
const userData = isError ? ({ loginStatus: "Error" } as const) : data
useTrackHardNavigation({
pageData,
hotelInfo,
paymentInfo,
ancillaries,
userData,
pathName,
})
useTrackSoftNavigation({
pageData,
hotelInfo,
paymentInfo,
ancillaries,
userData,
pathName,
})
return null
}

View File

@@ -0,0 +1,17 @@
"use client"
import { useParams } from "next/navigation"
import { Lang } from "@scandic-hotels/common/constants/language"
import { languageSchema } from "@scandic-hotels/common/utils/languages"
/**
* A hook to get the current lang from the URL
*/
export default function useLang() {
const { lang } = useParams<{
lang: Lang
}>()
const parsedLang = languageSchema.safeParse(lang)
return parsedLang.success ? parsedLang.data : Lang.en
}

View File

@@ -0,0 +1,19 @@
import type { Lang } from "@scandic-hotels/common/constants/language"
type NextSearchParams = { [key: string]: string | string[] | undefined }
type SearchParams = {
searchParams: Promise<NextSearchParams>
}
type Params<P = {}> = {
params: Promise<P>
}
export type LangParams = {
lang: Lang
}
export type LayoutArgs<P = undefined> = P extends undefined ? {} : Params<P>
export type PageArgs<P = undefined> = LayoutArgs<P> & SearchParams

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() { export default function AlternativeHotelsLoading() {
return <SelectHotelSkeleton /> return <SelectHotelSkeleton />

View File

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

View File

@@ -1,20 +1,7 @@
import stringify from "json-stable-stringify-without-jsonify" import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage"
import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import { Suspense } from "react"
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 TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { import {
type LangParams, type LangParams,
@@ -26,112 +13,18 @@ export default async function AlternativeHotelsPage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams 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 ( return (
<> <AlternativeHotelsPagePrimitive
<SelectHotel lang={lang}
bookingCode={booking.bookingCode} searchParams={searchParams}
city={searchDetails.city} renderTracking={(props) => (
hotels={hotels}
isAlternative={!!searchDetails.hotel}
isBookingCodeRateAvailable={isBookingCodeRateAvailable}
mapHref={mapHref}
title={title}
/>
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
<TrackingSDK <TrackingSDK
pageData={pageTrackingData} hotelInfo={props.hotelsTrackingData}
hotelInfo={hotelsTrackingData} pageData={props.pageTrackingData}
/> />
</Suspense> )}
</> />
) )
} }

View File

@@ -2,9 +2,10 @@ import { cookies } from "next/headers"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { Suspense } from "react" import { Suspense } from "react"
import FnFNotAllowedAlert from "@scandic-hotels/booking-flow/components/FnFNotAllowedAlert"
import { parseDetailsSearchParams } from "@scandic-hotels/booking-flow/utils/url" import { parseDetailsSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { FamilyAndFriendsCodes } from "@scandic-hotels/common/constants/familyAndFriends"
import { FamilyAndFriendsCodes } from "@/constants/booking"
import { import {
getBreakfastPackages, getBreakfastPackages,
getHotel, getHotel,
@@ -19,7 +20,6 @@ import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop" import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile" import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDetails/Tracking" import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDetails/Tracking"
import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert"
import RoomProvider from "@/providers/Details/RoomProvider" import RoomProvider from "@/providers/Details/RoomProvider"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider" 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 TrackingSDK from "@/components/TrackingSDK"
import styles from "./page.module.css" import styles from "./page.module.css"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelReservationPage( 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() { export default function SelectHotelLoading() {
return <SelectHotelSkeleton /> return <SelectHotelSkeleton />

View File

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

View File

@@ -1,19 +1,7 @@
import stringify from "json-stable-stringify-without-jsonify" import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import { Suspense } from "react"
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 TrackingSDK from "@/components/TrackingSDK"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" import { getLang } from "@/i18n/serverContext"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
@@ -21,93 +9,18 @@ export default async function SelectHotelPage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {
const searchParams = await props.searchParams 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 ( return (
<> <SelectHotelPagePrimitive
<SelectHotel lang={lang}
bookingCode={booking.bookingCode} searchParams={searchParams}
isBookingCodeRateAvailable={isBookingCodeRateAvailable} renderTracking={(props) => (
city={city}
hotels={hotels}
mapHref={mapHref}
title={city.name}
/>
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
<TrackingSDK <TrackingSDK
pageData={pageTrackingData} hotelInfo={props.hotelsTrackingData}
hotelInfo={hotelsTrackingData} pageData={props.pageTrackingData}
/> />
</Suspense> )}
</> />
) )
} }

View File

@@ -1,10 +1,10 @@
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import { import {
TrackingChannelEnum, TrackingChannelEnum,
type TrackingSDKPageData, 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() { export default async function Tracking() {
const lang = await getLang() const lang = await getLang()

View File

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

View File

@@ -1,10 +1,10 @@
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import { import {
TrackingChannelEnum, TrackingChannelEnum,
type TrackingSDKPageData, 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() { export default async function Tracking() {
const lang = await getLang() const lang = await getLang()

View File

@@ -1,7 +1,7 @@
import NextAuth, { type NextAuthConfig, type User } from "next-auth" import NextAuth, { type NextAuthConfig, type User } from "next-auth"
import { LoginTypeEnum } from "@scandic-hotels/common/constants/loginType"
import { logger } from "@scandic-hotels/common/logger" import { logger } from "@scandic-hotels/common/logger"
import { LoginTypeEnum } from "@scandic-hotels/trpc/types/loginType"
import { PRE_REFRESH_TIME_IN_SECONDS } from "@/constants/auth" import { PRE_REFRESH_TIME_IN_SECONDS } from "@/constants/auth"
import { env } from "@/env/server" import { env } from "@/env/server"

View File

@@ -0,0 +1,30 @@
"use client"
import { BookingFlowContextProvider } from "@scandic-hotels/booking-flow/BookingFlowContextProvider"
import { BookingFlowTrackingProvider } from "@scandic-hotels/booking-flow/BookingFlowTrackingProvider"
import { trackEvent } from "@scandic-hotels/common/tracking/base"
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"
import { trackAccordionClick, trackOpenSidePeekEvent } from "@/utils/tracking"
import { trackBookingSearchClick } from "@/utils/tracking/booking"
import type { ReactNode } from "react"
export function BookingFlowProviders({ children }: { children: ReactNode }) {
const isLoggedIn = useIsUserLoggedIn()
return (
<BookingFlowContextProvider data={{ isLoggedIn }}>
<BookingFlowTrackingProvider
trackingFunctions={{
trackBookingSearchClick,
trackAccordionItemOpen: trackAccordionClick,
trackOpenSidePeek: trackOpenSidePeekEvent,
trackGenericEvent: trackEvent,
}}
>
{children}
</BookingFlowTrackingProvider>
</BookingFlowContextProvider>
)
}

View File

@@ -6,7 +6,7 @@ import { useEffect } from "react"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
import { trackOpenMapView } from "@/utils/tracking/destinationPage" import { trackOpenMapView } from "@/utils/tracking/destinationPage"
import type { TrackingSDKPageData } from "@/types/components/tracking" import type { TrackingSDKPageData } from "@scandic-hotels/common/tracking/types"
interface DestinationTrackingProps { interface DestinationTrackingProps {
pageData: TrackingSDKPageData pageData: TrackingSDKPageData

View File

@@ -10,13 +10,12 @@ import {
} from "react" } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useHotelsMapStore } from "@scandic-hotels/booking-flow/stores/hotels-map"
import { debounce } from "@scandic-hotels/common/utils/debounce" import { debounce } from "@scandic-hotels/common/utils/debounce"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { InteractiveMap } from "@scandic-hotels/design-system/Map/InteractiveMap" import { InteractiveMap } from "@scandic-hotels/design-system/Map/InteractiveMap"
import { useHotelsMapStore } from "@/stores/hotels-map"
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn" import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"

View File

@@ -1,4 +1,9 @@
import { logger } from "@scandic-hotels/common/logger" import { logger } from "@scandic-hotels/common/logger"
import {
TrackingChannelEnum,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@scandic-hotels/common/tracking/types"
import { PointOfInterestGroupEnum } from "@scandic-hotels/trpc/enums/pointOfInterest" import { PointOfInterestGroupEnum } from "@scandic-hotels/trpc/enums/pointOfInterest"
import type { Lang } from "@scandic-hotels/common/constants/language" import type { Lang } from "@scandic-hotels/common/constants/language"
@@ -15,11 +20,6 @@ import type {
HotelPageSectionHeadings, HotelPageSectionHeadings,
HotelPageSections, HotelPageSections,
} from "@/types/components/hotelPage/sections" } from "@/types/components/hotelPage/sections"
import {
TrackingChannelEnum,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { HotelHashValues } from "@/types/enums/hotelPage" import { HotelHashValues } from "@/types/enums/hotelPage"
export function getRoomNameAsParam(roomName: string) { export function getRoomNameAsParam(roomName: string) {

View File

@@ -1,8 +1,8 @@
import type { TrackingSDKPageData } from "@scandic-hotels/common/tracking/types"
import type { CollectionPage } from "@scandic-hotels/trpc/types/collectionPage" import type { CollectionPage } from "@scandic-hotels/trpc/types/collectionPage"
import type { ContentPage } from "@scandic-hotels/trpc/types/contentPage" import type { ContentPage } from "@scandic-hotels/trpc/types/contentPage"
import type { VariantProps } from "class-variance-authority" import type { VariantProps } from "class-variance-authority"
import type { TrackingSDKPageData } from "@/types/components/tracking"
import type { staticPageVariants } from "./variants" import type { staticPageVariants } from "./variants"
export interface StaticPageProps export interface StaticPageProps

View File

@@ -9,7 +9,7 @@ import type {
SiteSectionObject, SiteSectionObject,
TrackingData, TrackingData,
TrackingProps, TrackingProps,
} from "@/types/components/tracking" } from "@scandic-hotels/common/tracking/types"
function createPageObject(trackingData: TrackingData) { function createPageObject(trackingData: TrackingData) {
const englishSegments = trackingData.englishUrl const englishSegments = trackingData.englishUrl

View File

@@ -30,7 +30,7 @@ import {
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput" import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
import { useFormTracking } from "@/components/TrackingSDK/hooks" import { useFormTracking } from "@/components/TrackingSDK/useFormTracking"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { getFormattedCountryList } from "@/utils/countries" import { getFormattedCountryList } from "@/utils/countries"
import { getErrorMessage } from "@/utils/getErrorMessage" import { getErrorMessage } from "@/utils/getErrorMessage"

View File

@@ -2,8 +2,15 @@ import { createHash } from "crypto"
import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import {
TrackingChannelEnum,
type TrackingSDKAncillaries,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
type TrackingSDKPaymentInfo,
} from "@scandic-hotels/common/tracking/types"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { RateEnum } from "@scandic-hotels/trpc/enums/rate"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { CancellationRuleEnum } from "@/constants/booking" import { CancellationRuleEnum } from "@/constants/booking"
@@ -17,13 +24,6 @@ import type { Lang } from "@scandic-hotels/common/constants/language"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation" import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { RateDefinition } from "@scandic-hotels/trpc/types/roomAvailability" import type { RateDefinition } from "@scandic-hotels/trpc/types/roomAvailability"
import {
TrackingChannelEnum,
type TrackingSDKAncillaries,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
type TrackingSDKPaymentInfo,
} from "@/types/components/tracking"
import type { Room } from "@/types/stores/booking-confirmation" import type { Room } from "@/types/stores/booking-confirmation"
function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) { function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) {

View File

@@ -13,7 +13,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests" import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests"
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import { useFormTracking } from "@/components/TrackingSDK/hooks" import { useFormTracking } from "@/components/TrackingSDK/useFormTracking"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import usePhoneNumberParsing from "@/hooks/usePhoneNumberParsing" import usePhoneNumberParsing from "@/hooks/usePhoneNumberParsing"

View File

@@ -13,7 +13,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests" import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/SpecialRequests"
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import { useFormTracking } from "@/components/TrackingSDK/hooks" import { useFormTracking } from "@/components/TrackingSDK/useFormTracking"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import usePhoneNumberParsing from "@/hooks/usePhoneNumberParsing" import usePhoneNumberParsing from "@/hooks/usePhoneNumberParsing"

View File

@@ -1,8 +1,8 @@
import { HotelDetailsSidePeek } from "@scandic-hotels/booking-flow/components/HotelDetailsSidePeek"
import Image from "@scandic-hotels/design-system/Image" import Image from "@scandic-hotels/design-system/Image"
import Title from "@scandic-hotels/design-system/Title" import Title from "@scandic-hotels/design-system/Title"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import styles from "./header.module.css" import styles from "./header.module.css"

View File

@@ -1,6 +1,12 @@
import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import {
TrackingChannelEnum,
type TrackingSDKAncillaries,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@scandic-hotels/common/tracking/types"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum" import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { PackageTypeEnum } from "@scandic-hotels/trpc/enums/packages" import { PackageTypeEnum } from "@scandic-hotels/trpc/enums/packages"
@@ -22,12 +28,6 @@ import type {
DetailsBooking, DetailsBooking,
RoomRate, RoomRate,
} from "@/types/components/hotelReservation/enterDetails/details" } from "@/types/components/hotelReservation/enterDetails/details"
import {
TrackingChannelEnum,
type TrackingSDKAncillaries,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type { RoomState } from "@/types/stores/enter-details" import type { RoomState } from "@/types/stores/enter-details"
export function getTracking( export function getTracking(

View File

@@ -1,10 +1,10 @@
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import { import {
TrackingChannelEnum, TrackingChannelEnum,
type TrackingSDKPageData, 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() { export default async function Tracking() {
const lang = await getLang() const lang = await getLang()

View File

@@ -2,6 +2,7 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats" import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
@@ -11,7 +12,6 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image" import Image from "@scandic-hotels/design-system/Image"
import Modal from "@scandic-hotels/design-system/Modal" import Modal from "@scandic-hotels/design-system/Modal"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { RateEnum } from "@scandic-hotels/trpc/enums/rate"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { CancellationRuleEnum } from "@/constants/booking" import { CancellationRuleEnum } from "@/constants/booking"

View File

@@ -1,10 +1,10 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { IconButton } from "@scandic-hotels/design-system/IconButton" import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Modal from "@scandic-hotels/design-system/Modal" import Modal from "@scandic-hotels/design-system/Modal"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { RateEnum } from "@scandic-hotels/trpc/enums/rate"
import { CancellationRuleEnum } from "@/constants/booking" import { CancellationRuleEnum } from "@/constants/booking"
import { useMyStayStore } from "@/stores/my-stay" import { useMyStayStore } from "@/stores/my-stay"

View File

@@ -10,7 +10,7 @@ import { convertToChildType } from "../../utils/convertToChildType"
import { getPriceType } from "../../utils/getPriceType" import { getPriceType } from "../../utils/getPriceType"
import { formatChildBedPreferences } from "../utils" import { formatChildBedPreferences } from "../utils"
import type { RateEnum } from "@scandic-hotels/trpc/enums/rate" import type { RateEnum } from "@scandic-hotels/common/constants/rate"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation" import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@scandic-hotels/trpc/types/hotel" import type { Room } from "@scandic-hotels/trpc/types/hotel"

View File

@@ -1,114 +0,0 @@
import { notFound } from "next/navigation"
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
import { env } from "@/env/server"
import { getCityCoordinates } from "@/lib/trpc/memoizedRequests"
import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { getHotelPins } from "../../HotelCardDialogListing/utils"
import { getFiltersFromHotels, getHotels } from "../helpers"
import { getSelectHotelTracking } from "../tracking"
import SelectHotelMap from "."
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
export async function SelectHotelMapContainer({
booking,
isAlternativeHotels,
}: SelectHotelMapContainerProps) {
const lang = await getLang()
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const getHotelSearchDetailsPromise = safeTry(
getHotelSearchDetails(booking, isAlternativeHotels)
)
const [searchDetails] = await getHotelSearchDetailsPromise
if (!searchDetails) {
return notFound()
}
const {
city,
cityIdentifier,
hotel: isAlternativeFor,
redemption,
} = searchDetails
if (!city) {
return notFound()
}
const hotels = await getHotels({
fromDate: booking.fromDate,
toDate: booking.toDate,
rooms: booking.rooms,
isAlternativeFor,
bookingCode: booking.bookingCode,
city,
redemption: !!redemption,
})
const hotelPins = getHotelPins(hotels)
const filterList = getFiltersFromHotels(hotels)
const cityCoordinates = await getCityCoordinates({
city: city.name,
hotel: { address: hotels?.[0]?.hotel?.address.streetAddress },
})
const arrivalDate = new Date(booking.fromDate)
const departureDate = new Date(booking.toDate)
const isRedemptionAvailability = redemption
? hotels.some(
(hotel) => hotel.availability.productType?.redemptions?.length
)
: false
const isBookingCodeRateAvailable = booking.bookingCode
? hotels?.some((hotel) => hotel.availability.bookingCode)
: false
const { hotelsTrackingData, pageTrackingData } = getSelectHotelTracking({
lang,
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
pageName: isAlternativeHotels
? "hotelreservation|alternative-hotels|mapview"
: "hotelreservation|select-hotel|mapview",
siteSections: isAlternativeHotels
? "hotelreservation|altervative-hotels|mapview"
: "hotelreservation|select-hotel|mapview",
arrivalDate,
departureDate,
rooms: booking.rooms,
hotelsResult: hotels.length,
searchTerm: isAlternativeFor ? booking.hotelId : cityIdentifier,
country: hotels?.[0]?.hotel.address.country,
hotelCity: hotels?.[0]?.hotel.address.city,
bookingCode: booking.bookingCode,
isBookingCodeRateAvailable,
isRedemption: redemption,
isRedemptionAvailable: isRedemptionAvailability,
})
return (
<>
<SelectHotelMap
apiKey={googleMapsApiKey}
hotelPins={hotelPins}
mapId={googleMapId}
hotels={hotels}
filterList={filterList}
cityCoordinates={cityCoordinates}
bookingCode={booking.bookingCode}
isBookingCodeRateAvailable={isBookingCodeRateAvailable}
isAlternativeHotels={isAlternativeHotels}
/>
<TrackingSDK pageData={pageTrackingData} hotelInfo={hotelsTrackingData} />
</>
)
}

View File

@@ -1,12 +1,12 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Body from "@scandic-hotels/design-system/Body" import Body from "@scandic-hotels/design-system/Body"
import Caption from "@scandic-hotels/design-system/Caption" import Caption from "@scandic-hotels/design-system/Caption"
import Footnote from "@scandic-hotels/design-system/Footnote" import Footnote from "@scandic-hotels/design-system/Footnote"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import Subtitle from "@scandic-hotels/design-system/Subtitle" import Subtitle from "@scandic-hotels/design-system/Subtitle"
import { RateEnum } from "@scandic-hotels/trpc/enums/rate"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop" import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn" import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"

View File

@@ -2,6 +2,7 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { logger } from "@scandic-hotels/common/logger" import { logger } from "@scandic-hotels/common/logger"
import Body from "@scandic-hotels/design-system/Body" import Body from "@scandic-hotels/design-system/Body"
import Caption from "@scandic-hotels/design-system/Caption" import Caption from "@scandic-hotels/design-system/Caption"
@@ -10,7 +11,6 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image" import Image from "@scandic-hotels/design-system/Image"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import Subtitle from "@scandic-hotels/design-system/Subtitle" import Subtitle from "@scandic-hotels/design-system/Subtitle"
import { RateEnum } from "@scandic-hotels/trpc/enums/rate"
import { useSelectRateContext } from "@/contexts/SelectRate/SelectRateContext" import { useSelectRateContext } from "@/contexts/SelectRate/SelectRateContext"
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn" import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"

View File

@@ -1,4 +1,4 @@
import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton" import { RoomCardSkeleton } from "@scandic-hotels/booking-flow/components/RoomCardSkeleton"
import styles from "./roomsListSkeleton.module.css" import styles from "./roomsListSkeleton.module.css"

View File

@@ -1,4 +1,4 @@
import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton" import { RoomCardSkeleton } from "@scandic-hotels/booking-flow/components/RoomCardSkeleton"
import styles from "./RoomsContainerSkeleton.module.css" import styles from "./RoomsContainerSkeleton.module.css"

View File

@@ -1,17 +1,19 @@
import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { Room } from "@/types/components/hotelReservation/selectRate/selectRate"
import { import {
TrackingChannelEnum, TrackingChannelEnum,
type TrackingSDKHotelInfo, type TrackingSDKHotelInfo,
type TrackingSDKPageData, type TrackingSDKPageData,
} from "@/types/components/tracking" } from "@scandic-hotels/common/tracking/types"
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails" import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { Room } from "@/types/components/hotelReservation/selectRate/selectRate"
type ChildrenInRoom = (Child[] | null)[] | null
type SelectRateTrackingInput = { type SelectRateTrackingInput = {
lang: Lang lang: Lang

View File

@@ -1,16 +1,15 @@
import { cookies } from "next/headers" import { cookies } from "next/headers"
import FnFNotAllowedAlert from "@scandic-hotels/booking-flow/components/FnFNotAllowedAlert"
import { HotelDetailsSidePeek } from "@scandic-hotels/booking-flow/components/HotelDetailsSidePeek"
import { FamilyAndFriendsCodes } from "@scandic-hotels/common/constants/familyAndFriends"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { HotelInfoCard } from "@scandic-hotels/design-system/HotelInfoCard" import { HotelInfoCard } from "@scandic-hotels/design-system/HotelInfoCard"
import { FamilyAndFriendsCodes } from "@/constants/booking"
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer" import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import FnFNotAllowedAlert from "../FnFNotAllowedAlert/FnFNotAllowedAlert"
import { hasOverlappingDates } from "../utils" import { hasOverlappingDates } from "../utils"
import AvailabilityError from "./AvailabilityError" import AvailabilityError from "./AvailabilityError"
import Tracking from "./Tracking" import Tracking from "./Tracking"

View File

@@ -8,10 +8,9 @@ import useLang from "@/hooks/useLang"
import { useLazyPathname } from "@/hooks/useLazyPathname" import { useLazyPathname } from "@/hooks/useLazyPathname"
import { trackLoginClick } from "@/utils/tracking" import { trackLoginClick } from "@/utils/tracking"
import type { TrackingPosition } from "@scandic-hotels/common/tracking/types"
import type { PropsWithChildren } from "react" import type { PropsWithChildren } from "react"
import type { TrackingPosition } from "@/types/components/tracking"
export default function LoginButton({ export default function LoginButton({
position, position,
trackingId, trackingId,

View File

@@ -3,8 +3,9 @@
import { usePathname, useSearchParams } from "next/navigation" import { usePathname, useSearchParams } from "next/navigation"
import { startTransition, useEffect } from "react" import { startTransition, useEffect } from "react"
import useRouterTransitionStore from "@/stores/router-transition" import { isSameBookingWidgetParams } from "@scandic-hotels/booking-flow/utils/isSameBooking"
import useTrackingStore from "@/stores/tracking" import useRouterTransitionStore from "@scandic-hotels/common/stores/router-transition"
import useTrackingStore from "@scandic-hotels/common/stores/tracking"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { trackPageViewStart } from "@/utils/tracking" import { trackPageViewStart } from "@/utils/tracking"
@@ -34,7 +35,10 @@ export default function RouteChange() {
} }
updateRouteInfo(pathName, currentLang, searchParams) updateRouteInfo(pathName, currentLang, searchParams)
if (hasPathOrLangChanged() || hasBookingFlowParamsChanged()) { if (
hasPathOrLangChanged() ||
hasBookingFlowParamsChanged(isSameBookingWidgetParams)
) {
setInitialPageLoadTime(Date.now()) setInitialPageLoadTime(Date.now())
trackPageViewStart() trackPageViewStart()
startTransition(() => { startTransition(() => {

View File

@@ -1,345 +0,0 @@
"use client"
import { usePathname } from "next/navigation"
import {
startTransition,
useCallback,
useEffect,
useRef,
useState,
} from "react"
import {
type Control,
type FieldValues,
useFormState,
type UseFromSubscribe,
} from "react-hook-form"
import { useSessionId } from "@scandic-hotels/common/hooks/useSessionId"
import { logger } from "@scandic-hotels/common/logger"
import { trpc } from "@scandic-hotels/trpc/client"
import useRouterTransitionStore from "@/stores/router-transition"
import useTrackingStore from "@/stores/tracking"
import useLang from "@/hooks/useLang"
import { promiseWithTimeout } from "@/utils/promiseWithTimeout"
import { createSDKPageObject, trackPageView } from "@/utils/tracking"
import {
type FormType,
trackFormAbandonment,
trackFormCompletion,
trackFormInputStarted,
} from "@/utils/tracking/form"
import type {
TrackingSDKProps,
TrackingSDKUserData,
} from "@/types/components/tracking"
enum TransitionStatusEnum {
NotRun = "NotRun",
Running = "Running",
Done = "Done",
}
type TransitionStatus = keyof typeof TransitionStatusEnum
let hasTrackedHardNavigation = false
export const useTrackHardNavigation = ({
pageData,
hotelInfo,
paymentInfo,
ancillaries,
}: TrackingSDKProps) => {
const lang = useLang()
const {
data: userTrackingData,
isPending,
isError,
} = trpc.user.userTrackingInfo.useQuery({ lang })
const sessionId = useSessionId()
const pathName = usePathname()
useEffect(() => {
if (isPending) {
return
}
const userData: TrackingSDKUserData = isError
? { loginStatus: "Error" }
: userTrackingData
if (hasTrackedHardNavigation) {
return
}
hasTrackedHardNavigation = true
const track = () => {
trackPerformance({
pathName,
sessionId,
paymentInfo,
hotelInfo,
userData,
pageData,
ancillaries,
})
}
if (document.readyState === "complete") {
track()
return
}
window.addEventListener("load", track)
return () => window.removeEventListener("load", track)
}, [
isError,
pathName,
hotelInfo,
userTrackingData,
pageData,
sessionId,
paymentInfo,
isPending,
ancillaries,
])
}
export const useTrackSoftNavigation = ({
pageData,
hotelInfo,
paymentInfo,
ancillaries,
}: TrackingSDKProps) => {
const lang = useLang()
const {
data: userTrackingData,
isPending,
isError,
} = trpc.user.userTrackingInfo.useQuery({ lang })
const [status, setStatus] = useState<TransitionStatus>(
TransitionStatusEnum.NotRun
)
const { getPageLoadTime } = useTrackingStore()
const sessionId = useSessionId()
const pathName = usePathname()
const { isTransitioning, stopRouterTransition } = useRouterTransitionStore()
const previousPathname = useRef<string | null>(null)
useEffect(() => {
if (isPending) {
return
}
if (isTransitioning && status === TransitionStatusEnum.NotRun) {
startTransition(() => {
setStatus(TransitionStatusEnum.Running)
})
return
}
if (isTransitioning && status === TransitionStatusEnum.Running) {
setStatus(TransitionStatusEnum.Done)
stopRouterTransition()
return
}
if (!isTransitioning && status === TransitionStatusEnum.Done) {
const pageLoadTime = getPageLoadTime()
const trackingData = {
...pageData,
sessionId,
pathName,
pageLoadTime: pageLoadTime,
}
const pageObject = createSDKPageObject(trackingData)
const userData: TrackingSDKUserData = isError
? { loginStatus: "Error" }
: userTrackingData
trackPageView({
event: "pageView",
pageInfo: pageObject,
userInfo: userData,
hotelInfo: hotelInfo,
paymentInfo,
ancillaries,
})
setStatus(TransitionStatusEnum.NotRun) // Reset status
previousPathname.current = pathName // Update for next render
}
}, [
isError,
isPending,
isTransitioning,
status,
stopRouterTransition,
pageData,
pathName,
hotelInfo,
getPageLoadTime,
sessionId,
paymentInfo,
userTrackingData,
ancillaries,
])
}
const trackPerformance = async ({
pathName,
sessionId,
paymentInfo,
hotelInfo,
userData,
pageData,
ancillaries,
}: {
pathName: string
sessionId: string | null
paymentInfo: TrackingSDKProps["paymentInfo"]
hotelInfo: TrackingSDKProps["hotelInfo"]
userData: TrackingSDKUserData
pageData: TrackingSDKProps["pageData"]
ancillaries: TrackingSDKProps["ancillaries"]
}) => {
let pageLoadTime: number | undefined = undefined
let lcpTime: number | undefined = undefined
try {
pageLoadTime = await promiseWithTimeout(getPageLoadTimeEntry(), 3000)
} catch (error) {
logger.error("Error obtaining pageLoadTime:", error)
}
try {
lcpTime = await promiseWithTimeout(getLCPTimeEntry(), 3000)
} catch (error) {
logger.error("Error obtaining lcpTime:", error)
}
const trackingData = {
...pageData,
sessionId,
pathName,
pageLoadTime,
lcpTime,
}
const pageObject = createSDKPageObject(trackingData)
trackPageView({
event: "pageView",
pageInfo: pageObject,
userInfo: userData,
hotelInfo,
paymentInfo,
ancillaries,
})
}
const getLCPTimeEntry = () => {
return new Promise<number | undefined>((resolve) => {
const observer = new PerformanceObserver((entries) => {
const lastEntry = entries.getEntries().at(-1)
if (lastEntry) {
observer.disconnect()
resolve(lastEntry.startTime / 1000)
}
})
const lcpSupported = PerformanceObserver.supportedEntryTypes?.includes(
"largest-contentful-paint"
)
if (lcpSupported) {
observer.observe({
type: "largest-contentful-paint",
buffered: true,
})
} else {
resolve(undefined)
}
})
}
const getPageLoadTimeEntry = () => {
return new Promise<number>((resolve) => {
const observer = new PerformanceObserver((entries) => {
const navEntry = entries.getEntriesByType("navigation")[0]
if (navEntry) {
observer.disconnect()
resolve(navEntry.duration / 1000)
}
})
observer.observe({ type: "navigation", buffered: true })
})
}
export function useFormTracking<T extends FieldValues>(
formType: FormType,
subscribe: UseFromSubscribe<T>,
control: Control<T>,
nameSuffix: string = ""
) {
const [formStarted, setFormStarted] = useState(false)
const lastAccessedField = useRef<string | undefined>(undefined)
const formState = useFormState({ control })
useEffect(() => {
const unsubscribe = subscribe({
formState: { dirtyFields: true },
callback: (data) => {
if ("name" in data) {
lastAccessedField.current = data.name as string
}
if (!formStarted) {
trackFormInputStarted(formType, nameSuffix)
setFormStarted(true)
}
},
})
return () => unsubscribe()
}, [subscribe, formType, nameSuffix, formStarted])
useEffect(() => {
if (!formStarted || !lastAccessedField.current || formState.isValid) return
const lastField = lastAccessedField.current
function handleBeforeUnload() {
trackFormAbandonment(formType, lastField, nameSuffix)
}
function handleVisibilityChange() {
if (document.visibilityState === "hidden") {
trackFormAbandonment(formType, lastField, nameSuffix)
}
}
window.addEventListener("beforeunload", handleBeforeUnload)
window.addEventListener("visibilitychange", handleVisibilityChange)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
window.removeEventListener("visibilitychange", handleVisibilityChange)
}
}, [formStarted, formType, nameSuffix, formState.isValid])
const trackFormSubmit = useCallback(() => {
if (formState.isValid) {
trackFormCompletion(formType, nameSuffix)
}
}, [formType, nameSuffix, formState.isValid])
return {
trackFormSubmit,
}
}

View File

@@ -1,16 +1,19 @@
"use client" "use client"
import { import { usePathname } from "next/navigation"
useTrackHardNavigation,
useTrackSoftNavigation, import { useTrackHardNavigation } from "@scandic-hotels/common/tracking/useTrackHardNavigation"
} from "@/components/TrackingSDK/hooks" import { useTrackSoftNavigation } from "@scandic-hotels/common/tracking/useTrackSoftNavigation"
import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "@/hooks/useLang"
import type { import type {
TrackingSDKAncillaries, TrackingSDKAncillaries,
TrackingSDKHotelInfo, TrackingSDKHotelInfo,
TrackingSDKPageData, TrackingSDKPageData,
TrackingSDKPaymentInfo, TrackingSDKPaymentInfo,
} from "@/types/components/tracking" } from "@scandic-hotels/common/tracking/types"
export default function TrackingSDK({ export default function TrackingSDK({
pageData, pageData,
@@ -23,8 +26,30 @@ export default function TrackingSDK({
paymentInfo?: TrackingSDKPaymentInfo paymentInfo?: TrackingSDKPaymentInfo
ancillaries?: TrackingSDKAncillaries ancillaries?: TrackingSDKAncillaries
}) { }) {
useTrackHardNavigation({ pageData, hotelInfo, paymentInfo, ancillaries }) const lang = useLang()
useTrackSoftNavigation({ pageData, hotelInfo, paymentInfo, ancillaries }) const pathName = usePathname()
const { data, isError } = trpc.user.userTrackingInfo.useQuery({
lang,
})
const userData = isError ? ({ loginStatus: "Error" } as const) : data
useTrackHardNavigation({
pageData,
hotelInfo,
paymentInfo,
ancillaries,
userData,
pathName,
})
useTrackSoftNavigation({
pageData,
hotelInfo,
paymentInfo,
ancillaries,
userData,
pathName,
})
return null return null
} }

View File

@@ -0,0 +1,78 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import {
type Control,
type FieldValues,
useFormState,
type UseFromSubscribe,
} from "react-hook-form"
import {
type FormType,
trackFormAbandonment,
trackFormCompletion,
trackFormInputStarted,
} from "@/utils/tracking/form"
export function useFormTracking<T extends FieldValues>(
formType: FormType,
subscribe: UseFromSubscribe<T>,
control: Control<T>,
nameSuffix: string = ""
) {
const [formStarted, setFormStarted] = useState(false)
const lastAccessedField = useRef<string | undefined>(undefined)
const formState = useFormState({ control })
useEffect(() => {
const unsubscribe = subscribe({
formState: { dirtyFields: true },
callback: (data) => {
if ("name" in data) {
lastAccessedField.current = data.name as string
}
if (!formStarted) {
trackFormInputStarted(formType, nameSuffix)
setFormStarted(true)
}
},
})
return () => unsubscribe()
}, [subscribe, formType, nameSuffix, formStarted])
useEffect(() => {
if (!formStarted || !lastAccessedField.current || formState.isValid) return
const lastField = lastAccessedField.current
function handleBeforeUnload() {
trackFormAbandonment(formType, lastField, nameSuffix)
}
function handleVisibilityChange() {
if (document.visibilityState === "hidden") {
trackFormAbandonment(formType, lastField, nameSuffix)
}
}
window.addEventListener("beforeunload", handleBeforeUnload)
window.addEventListener("visibilitychange", handleVisibilityChange)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
window.removeEventListener("visibilitychange", handleVisibilityChange)
}
}, [formStarted, formType, nameSuffix, formState.isValid])
const trackFormSubmit = useCallback(() => {
if (formState.isValid) {
trackFormCompletion(formType, nameSuffix)
}
}, [formType, nameSuffix, formState.isValid])
return {
trackFormSubmit,
}
}

View File

@@ -1,7 +1,5 @@
import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod" import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
export const FamilyAndFriendsCodes = ["D000029555", "D000029271", "D000029195"]
export const SEARCHTYPE = "searchtype" export const SEARCHTYPE = "searchtype"
export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError" export const MEMBERSHIP_FAILED_ERROR = "MembershipFailedError"

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { RateEnum } from "@scandic-hotels/trpc/enums/rate" import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { isRateSelected } from "./isRateSelected" import { isRateSelected } from "./isRateSelected"

View File

@@ -1,4 +1,4 @@
import type { RateEnum } from "@scandic-hotels/trpc/enums/rate" import type { RateEnum } from "@scandic-hotels/common/constants/rate"
import type { Rate } from "./types" import type { Rate } from "./types"

View File

@@ -1,8 +1,5 @@
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { isDefined } from "@scandic-hotels/common/utils/isDefined"
import { getLang } from "@/i18n/serverContext"
import { cache } from "@/utils/cache" import { cache } from "@/utils/cache"
import { serverClient } from "../server" import { serverClient } from "../server"
@@ -12,22 +9,13 @@ import type { GetHotelsByCSFilterInput } from "@scandic-hotels/trpc/routers/hote
import type { GetSavedPaymentCardsInput } from "@scandic-hotels/trpc/routers/user/input" import type { GetSavedPaymentCardsInput } from "@scandic-hotels/trpc/routers/user/input"
import type { RoomsAvailabilityExtendedInputSchema } from "@scandic-hotels/trpc/types/availability" import type { RoomsAvailabilityExtendedInputSchema } from "@scandic-hotels/trpc/types/availability"
import type { Country } from "@scandic-hotels/trpc/types/country" import type { Country } from "@scandic-hotels/trpc/types/country"
import type { import type { HotelInput } from "@scandic-hotels/trpc/types/hotel"
CityCoordinatesInput,
HotelInput,
} from "@scandic-hotels/trpc/types/hotel"
import type { import type {
AncillaryPackagesInput, AncillaryPackagesInput,
BreackfastPackagesInput, BreackfastPackagesInput,
PackagesInput, PackagesInput,
} from "@scandic-hotels/trpc/types/packages" } from "@scandic-hotels/trpc/types/packages"
export const getLocations = cache(async function getMemoizedLocations() {
const lang = await getLang()
const caller = await serverClient()
return caller.hotel.locations.get({ lang })
})
export const getProfile = cache(async function getMemoizedProfile() { export const getProfile = cache(async function getMemoizedProfile() {
const caller = await serverClient() const caller = await serverClient()
return caller.user.get() return caller.user.get()
@@ -187,13 +175,6 @@ export const getLinkedReservations = cache(
} }
) )
export const getCityCoordinates = cache(
async function getMemoizedCityCoordinates(input: CityCoordinatesInput) {
const caller = await serverClient()
return caller.hotel.map.city(input)
}
)
export const getCurrentRewards = cache( export const getCurrentRewards = cache(
async function getMemoizedCurrentRewards() { async function getMemoizedCurrentRewards() {
const caller = await serverClient() const caller = await serverClient()
@@ -294,83 +275,6 @@ export const getJobylonFeed = cache(async function getMemoizedJobylonFeed() {
return caller.partner.jobylon.feed.get() return caller.partner.jobylon.feed.get()
}) })
export const getJumpToData = cache(async function getMemoizedJumpToData() {
const lang = await getLang()
const caller = await serverClient()
const [locationsResults, urlsResults] = await Promise.allSettled([
getLocations(),
caller.hotel.locations.urls({ lang }),
])
if (
locationsResults.status === "fulfilled" &&
urlsResults.status === "fulfilled"
) {
const locations = locationsResults.value
const urls = urlsResults.value
if (!locations || !urls) {
return null
}
return locations
.map((location) => {
const { id, name, type } = location
const isCity = type === "cities"
const isHotel = type === "hotels"
let url: string | undefined
if (isCity) {
url = urls.cities.find(
(c) =>
c.city &&
location.cityIdentifier &&
c.city === location.cityIdentifier
)?.url
} else if (isHotel) {
url = urls.hotels.find(
(h) => h.hotelId && location.id && h.hotelId === location.id
)?.url
}
if (!url) {
return null
}
let description = ""
if (isCity) {
description = location.country
} else if (isHotel) {
description = location.relationships.city.name
}
const rankingNames: string[] = [location.name]
if (isCity) {
if (location.cityIdentifier) {
rankingNames.push(location.cityIdentifier)
}
}
const rankingKeywords = location.keyWords || []
return {
id,
displayName: name,
type,
description,
url,
rankingNames: rankingNames.map((v) => v.toLowerCase()),
rankingKeywords: rankingKeywords.map((v) => v.toLowerCase()),
}
})
.filter(isDefined)
}
return null
})
export const getSelectedRoomsAvailabilityEnterDetails = cache( export const getSelectedRoomsAvailabilityEnterDetails = cache(
async function getMemoizedSelectedRoomsAvailability( async function getMemoizedSelectedRoomsAvailability(
input: RoomsAvailabilityExtendedInputSchema input: RoomsAvailabilityExtendedInputSchema

View File

@@ -2,11 +2,11 @@
import deepmerge from "deepmerge" import deepmerge from "deepmerge"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import { isSameBooking } from "@scandic-hotels/booking-flow/utils/isSameBooking"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { createDetailsStore } from "@/stores/enter-details" import { createDetailsStore } from "@/stores/enter-details"
import { import {
checkIsSameBooking as checkIsSameBooking,
clearSessionStorage, clearSessionStorage,
getTotalPrice, getTotalPrice,
readFromSessionStorage, readFromSessionStorage,
@@ -89,8 +89,8 @@ export default function EnterDetailsProvider({
setHasInitializedStore(true) setHasInitializedStore(true)
return return
} }
const isSameBooking = checkIsSameBooking(storedValues.booking, booking)
if (!isSameBooking) { if (!isSameBooking(storedValues.booking, booking)) {
clearSessionStorage() clearSessionStorage()
setHasInitializedStore(true) setHasInitializedStore(true)
return return

View File

@@ -48,33 +48,6 @@ export function extractGuestFromUser(user: NonNullable<SafeUser>) {
} }
} }
export function checkIsSameBooking(
prev: (SelectRateBooking | BookingWidgetSearchData) & { errorCode?: string },
next: (SelectRateBooking | BookingWidgetSearchData) & { errorCode?: string }
) {
const { rooms: prevRooms, errorCode: prevErrorCode, ...prevBooking } = prev
const prevRoomsWithoutRateCodes = prevRooms?.map(
({ adults, childrenInRoom }) => ({ adults, childrenInRoom })
)
const { rooms: nextRooms, errorCode: nextErrorCode, ...nextBooking } = next
const nextRoomsWithoutRateCodes = nextRooms?.map(
({ adults, childrenInRoom }) => ({ adults, childrenInRoom })
)
return isEqual(
{
...prevBooking,
rooms: prevRoomsWithoutRateCodes,
},
{
...nextBooking,
rooms: nextRoomsWithoutRateCodes,
}
)
}
export function add(...nums: (number | string | undefined)[]) { export function add(...nums: (number | string | undefined)[]) {
return nums.reduce((total: number, num) => { return nums.reduce((total: number, num) => {
if (typeof num === "undefined") { if (typeof num === "undefined") {

View File

@@ -1,18 +0,0 @@
export type AvailabilityInput = {
cityId: string
roomStayStartDate: string
roomStayEndDate: string
adults: number
children?: string
bookingCode?: string
redemption?: boolean
}
export type AlternativeHotelsAvailabilityInput = {
roomStayStartDate: string
roomStayEndDate: string
adults: number
children?: string
bookingCode?: string
redemption?: boolean
}

View File

@@ -1,6 +0,0 @@
import type { CategorizedHotelFilters } from "./hotelFilters"
export type FilterAndSortModalProps = {
filters: CategorizedHotelFilters
setShowSkeleton?: (showSkeleton: boolean) => void
}

View File

@@ -1,7 +0,0 @@
export type FilterCheckboxProps = {
name: string
id: string
isDisabled?: boolean
isSelected: boolean
onChange: (filterId: string) => void
}

View File

@@ -1,25 +0,0 @@
import type { ProductType } from "@scandic-hotels/trpc/types/availability"
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
export enum HotelCardListingTypeEnum {
MapListing = "mapListing",
PageListing = "pageListing",
}
export type HotelData = {
hotelData: Hotel
price: ProductType
}
export type HotelCardListingProps = {
hotelData: HotelResponse[]
unfilteredHotelCount: number
type?: HotelCardListingTypeEnum
isAlternative?: boolean
}
export interface NullableHotelData extends Omit<HotelData, "hotelData"> {
hotelData: HotelData["hotelData"] | null
}

View File

@@ -1,11 +0,0 @@
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
import type { HotelCardListingTypeEnum } from "./hotelCardListingProps"
export type HotelCardProps = {
hotelData: HotelResponse
isUserLoggedIn: boolean
type?: HotelCardListingTypeEnum
state?: "default" | "active"
bookingCode?: string | null
isAlternative?: boolean
}

View File

@@ -0,0 +1,11 @@
import type { ProductType } from "@scandic-hotels/trpc/types/availability"
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
export type HotelData = {
hotelData: Hotel
price: ProductType
}
export interface NullableHotelData extends Omit<HotelData, "hotelData"> {
hotelData: HotelData["hotelData"] | null
}

View File

@@ -4,26 +4,6 @@ import type { Amenities } from "@scandic-hotels/trpc/types/hotel"
import type { z } from "zod" import type { z } from "zod"
import type { Coordinates } from "@/types/components/maps/coordinates" import type { Coordinates } from "@/types/components/maps/coordinates"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
import type { CategorizedHotelFilters } from "./hotelFilters"
import type { SelectHotelBooking } from "./selectHotel"
export interface HotelListingProps {
hotels: HotelResponse[]
unfilteredHotelCount: number
}
export interface SelectHotelMapProps {
apiKey: string
hotelPins: HotelPin[]
mapId: string
hotels: HotelResponse[]
filterList: CategorizedHotelFilters
cityCoordinates: Coordinates
bookingCode: string | undefined
isBookingCodeRateAvailable?: boolean
isAlternativeHotels?: boolean
}
type ImageSizes = z.infer<typeof imageSchema>["imageSizes"] type ImageSizes = z.infer<typeof imageSchema>["imageSizes"]
type ImageMetaData = z.infer<typeof imageSchema>["metaData"] type ImageMetaData = z.infer<typeof imageSchema>["metaData"]
@@ -56,13 +36,3 @@ export interface HotelCardDialogProps {
data: HotelPin data: HotelPin
handleClose: (event: { stopPropagation: () => void }) => void handleClose: (event: { stopPropagation: () => void }) => void
} }
export interface HotelCardDialogListingProps {
hotels: HotelResponse[]
unfilteredHotelCount: number
}
export type SelectHotelMapContainerProps = {
booking: SelectHotelBooking
isAlternativeHotels?: boolean
}

View File

@@ -1,10 +0,0 @@
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
export type NoAvailabilityAlertProp = {
hotelsLength: number
bookingCode?: string
isAllUnavailable: boolean
isAlternative?: boolean
isBookingCodeRateNotAvailable?: boolean
operaId: Hotel["operaId"]
}

View File

@@ -1,5 +1,5 @@
import type { BookingSearchType } from "@scandic-hotels/booking-flow/searchType" import type { BookingSearchType } from "@scandic-hotels/booking-flow/searchType"
import type { RateEnum } from "@scandic-hotels/trpc/enums/rate" import type { RateEnum } from "@scandic-hotels/common/constants/rate"
import type { Child } from "@scandic-hotels/trpc/types/child" import type { Child } from "@scandic-hotels/trpc/types/child"
import type { PackageEnum, Packages } from "@scandic-hotels/trpc/types/packages" import type { PackageEnum, Packages } from "@scandic-hotels/trpc/types/packages"
import type { import type {

View File

@@ -3,7 +3,7 @@
import { trackEvent } from "@scandic-hotels/common/tracking/base" import { trackEvent } from "@scandic-hotels/common/tracking/base"
import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast" import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast"
import type { LowestRoomPriceEvent } from "@/types/components/tracking" import type { LowestRoomPriceEvent } from "@scandic-hotels/common/tracking/types"
export function trackLowestRoomPrice(event: LowestRoomPriceEvent) { export function trackLowestRoomPrice(event: LowestRoomPriceEvent) {
trackEvent({ trackEvent({

View File

@@ -11,10 +11,10 @@ export {
trackLoginClick, trackLoginClick,
trackSocialMediaClick, trackSocialMediaClick,
} from "./navigation" } from "./navigation"
export { trackPaymentEvent, trackUpdatePaymentMethod } from "./payment"
export { trackClick } from "@scandic-hotels/common/tracking/base"
export { export {
createSDKPageObject, createSDKPageObject,
trackPageView, trackPageView,
trackPageViewStart, trackPageViewStart,
} from "./pageview" } from "@scandic-hotels/common/tracking/pageview"
export { trackPaymentEvent, trackUpdatePaymentMethod } from "./payment"
export { trackClick } from "@scandic-hotels/common/tracking/base"

View File

@@ -1,6 +1,6 @@
import { trackEvent } from "@scandic-hotels/common/tracking/base" import { trackEvent } from "@scandic-hotels/common/tracking/base"
import type { TrackingPosition } from "@/types/components/tracking" import type { TrackingPosition } from "@scandic-hotels/common/tracking/types"
export function trackFooterClick(group: string, name: string) { export function trackFooterClick(group: string, name: string) {
trackEvent({ trackEvent({

View File

@@ -3,7 +3,7 @@ import { trackEvent } from "@scandic-hotels/common/tracking/base"
import type { import type {
PaymentEvent, PaymentEvent,
PaymentFailEvent, PaymentFailEvent,
} from "@/types/components/tracking" } from "@scandic-hotels/common/tracking/types"
function isPaymentFailEvent(event: PaymentEvent): event is PaymentFailEvent { function isPaymentFailEvent(event: PaymentEvent): event is PaymentFailEvent {
return "errorMessage" in event return "errorMessage" in event

View File

@@ -9,10 +9,15 @@ export const env = createEnv({
*/ */
isServer: typeof window === "undefined" || "Deno" in window, isServer: typeof window === "undefined" || "Deno" in window,
server: { server: {
FOO: z.string().optional(), GOOGLE_STATIC_MAP_KEY: z.string(),
GOOGLE_STATIC_MAP_SIGNATURE_SECRET: z.string(),
GOOGLE_DYNAMIC_MAP_ID: z.string(),
}, },
emptyStringAsUndefined: true, emptyStringAsUndefined: true,
runtimeEnv: { runtimeEnv: {
FOO: process.env.FOO, GOOGLE_STATIC_MAP_KEY: process.env.GOOGLE_STATIC_MAP_KEY,
GOOGLE_STATIC_MAP_SIGNATURE_SECRET:
process.env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET,
GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID,
}, },
}) })

View File

@@ -0,0 +1,23 @@
"use client"
import { createContext, useContext } from "react"
export type BookingFlowContextData = {
isLoggedIn: boolean
}
export const BookingFlowContext = createContext<
BookingFlowContextData | undefined
>(undefined)
export const useBookingFlowContext = (): BookingFlowContextData => {
const context = useContext(BookingFlowContext)
if (!context) {
throw new Error(
"useBookingFlowContext must be used within a BookingFlowContextProvider. Did you forget to use the provider in the consuming app?"
)
}
return context
}

View File

@@ -0,0 +1,21 @@
"use client"
import { useIntl } from "react-intl"
export function AlternativeHotelsPageTitle({
hotelName,
}: {
hotelName: string
}) {
const intl = useIntl()
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return <>{title}</>
}

View File

@@ -0,0 +1,17 @@
"use client"
import {
BookingFlowContext,
type BookingFlowContextData,
} from "../bookingFlowContext"
import type { ReactNode } from "react"
export function BookingFlowContextProvider(props: {
children: ReactNode
data: BookingFlowContextData
}) {
return (
<BookingFlowContext value={props.data}>{props.children}</BookingFlowContext>
)
}

View File

@@ -1,12 +1,14 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert" import { Alert } from "@scandic-hotels/design-system/Alert"
import { getIntl } from "@/i18n"
import styles from "./FnFNotAllowedAlert.module.css" import styles from "./FnFNotAllowedAlert.module.css"
export default async function FnFNotAllowedAlert() { export default function FnFNotAllowedAlert() {
const intl = await getIntl() const intl = useIntl()
return ( return (
<div className={styles.fnfMain}> <div className={styles.fnfMain}>

View File

@@ -3,15 +3,19 @@
import { useCallback, useEffect, useRef } from "react" import { useCallback, useEffect, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useHotelFilterStore } from "@/stores/hotel-filters" import { useHotelFilterStore } from "../../stores/hotel-filters"
import { useHotelsMapStore } from "@/stores/hotels-map" import { useHotelsMapStore } from "../../stores/hotels-map"
import ListingHotelCardDialog from "../ListingHotelCardDialog"
import ListingHotelCardDialog from "../HotelCardDialog/ListingHotelCardDialog"
import { getHotelPins } from "./utils" import { getHotelPins } from "./utils"
import styles from "./hotelCardDialogListing.module.css" import styles from "./hotelCardDialogListing.module.css"
import type { HotelCardDialogListingProps } from "@/types/components/hotelReservation/selectHotel/map" import type { HotelResponse } from "../SelectHotel/helpers"
interface HotelCardDialogListingProps {
hotels: HotelResponse[]
unfilteredHotelCount: number
}
export default function HotelCardDialogListing({ export default function HotelCardDialogListing({
hotels, hotels,

View File

@@ -1,5 +1,39 @@
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" import type { imageSchema } from "@scandic-hotels/trpc/routers/hotels/schemas/image"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" import type { ProductTypeCheque } from "@scandic-hotels/trpc/types/availability"
import type { Amenities } from "@scandic-hotels/trpc/types/hotel"
import type { z } from "zod"
import type { HotelResponse } from "../SelectHotel/helpers"
type ImageSizes = z.infer<typeof imageSchema>["imageSizes"]
type ImageMetaData = z.infer<typeof imageSchema>["metaData"]
interface Coordinates {
lat: number
lng: number
}
export type HotelPin = {
bookingCode?: string | null
name: string
coordinates: Coordinates
chequePrice: ProductTypeCheque["localPrice"] | null
publicPrice: number | null
memberPrice: number | null
redemptionPrice: number | null
voucherPrice: number | null
rateType: string | null
currency: string
images: {
imageSizes: ImageSizes
metaData: ImageMetaData
}[]
amenities: Amenities
ratings: number | null
operaId: string
facilityIds: number[]
hasEnoughPoints: boolean
}
export function getHotelPins( export function getHotelPins(
hotels: HotelResponse[], hotels: HotelResponse[],

View File

@@ -1,14 +1,9 @@
"use client" "use client"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { useEffect, useMemo, useRef } from "react" import { useEffect, useMemo, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import {
BookingCodeFilterEnum,
useBookingCodeFilterStore,
} from "@scandic-hotels/booking-flow/stores/bookingCode-filter"
import { import {
alternativeHotelsMap, alternativeHotelsMap,
selectHotelMap, selectHotelMap,
@@ -17,14 +12,16 @@ import { useScrollToTop } from "@scandic-hotels/common/hooks/useScrollToTop"
import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton" import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton"
import { HotelCard } from "@scandic-hotels/design-system/HotelCard" import { HotelCard } from "@scandic-hotels/design-system/HotelCard"
import { useHotelFilterStore } from "@/stores/hotel-filters" import { useIsLoggedIn } from "../../hooks/useIsLoggedIn"
import { useHotelsMapStore } from "@/stores/hotels-map" import useLang from "../../hooks/useLang"
import { mapApiImagesToGalleryImages } from "../../misc/imageGallery"
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek" import {
import useLang from "@/hooks/useLang" BookingCodeFilterEnum,
import { isValidClientSession } from "@/utils/clientSession" useBookingCodeFilterStore,
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" } from "../../stores/bookingCode-filter"
import { useHotelFilterStore } from "../../stores/hotel-filters"
import { useHotelsMapStore } from "../../stores/hotels-map"
import { HotelDetailsSidePeek } from "../HotelDetailsSidePeek"
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
import { getSortedHotels } from "./utils" import { getSortedHotels } from "./utils"
@@ -32,10 +29,19 @@ import styles from "./hotelCardListing.module.css"
import type { HotelType } from "@scandic-hotels/common/constants/hotelType" import type { HotelType } from "@scandic-hotels/common/constants/hotelType"
import { import type { HotelResponse } from "../SelectHotel/helpers"
type HotelCardListingProps,
HotelCardListingTypeEnum, export enum HotelCardListingTypeEnum {
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" MapListing = "mapListing",
PageListing = "pageListing",
}
type HotelCardListingProps = {
hotelData: HotelResponse[]
unfilteredHotelCount: number
type?: HotelCardListingTypeEnum
isAlternative?: boolean
}
export default function HotelCardListing({ export default function HotelCardListing({
hotelData, hotelData,
@@ -46,8 +52,7 @@ export default function HotelCardListing({
const router = useRouter() const router = useRouter()
const lang = useLang() const lang = useLang()
const intl = useIntl() const intl = useIntl()
const { data: session } = useSession() const isUserLoggedIn = useIsLoggedIn()
const isUserLoggedIn = isValidClientSession(session)
const searchParams = useSearchParams() const searchParams = useSearchParams()
const activeFilters = useHotelFilterStore((state) => state.activeFilters) const activeFilters = useHotelFilterStore((state) => state.activeFilters)
const setResultCount = useHotelFilterStore((state) => state.setResultCount) const setResultCount = useHotelFilterStore((state) => state.setResultCount)

View File

@@ -1,5 +1,6 @@
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter" import { SortOrder } from "../../misc/sortOrder"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
import type { HotelResponse } from "../SelectHotel/helpers"
function getPricePerNight(hotel: HotelResponse): number { function getPricePerNight(hotel: HotelResponse): number {
return ( return (

View File

@@ -1,17 +1,17 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import AdditionalAmenities from "@scandic-hotels/booking-flow/components/AdditionalAmenities"
import Contact from "@scandic-hotels/booking-flow/components/Contact"
import BreakfastAccordionItem from "@scandic-hotels/booking-flow/components/SidePeekAccordions/BreakfastAccordionItem"
import CheckInCheckOutAccordionItem from "@scandic-hotels/booking-flow/components/SidePeekAccordions/CheckInCheckOutAccordionItem"
import ParkingAccordionItem from "@scandic-hotels/booking-flow/components/SidePeekAccordions/ParkingAccordionItem"
import Accordion from "@scandic-hotels/design-system/Accordion" import Accordion from "@scandic-hotels/design-system/Accordion"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem" import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink" import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName" import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { trackAccordionClick } from "@/utils/tracking" import { useTrackingContext } from "../../../trackingContext"
import AdditionalAmenities from "../../AdditionalAmenities"
import Contact from "../../Contact"
import BreakfastAccordionItem from "../../SidePeekAccordions/BreakfastAccordionItem"
import CheckInCheckOutAccordionItem from "../../SidePeekAccordions/CheckInCheckOutAccordionItem"
import ParkingAccordionItem from "../../SidePeekAccordions/ParkingAccordionItem"
import styles from "./hotelSidePeek.module.css" import styles from "./hotelSidePeek.module.css"
@@ -82,6 +82,7 @@ function AccessibilityAccordionItem({
elevatorPitch, elevatorPitch,
}: AccessibilityAccordionItemProps) { }: AccessibilityAccordionItemProps) {
const intl = useIntl() const intl = useIntl()
const tracking = useTrackingContext()
if (!elevatorPitch) { if (!elevatorPitch) {
return null return null
@@ -95,7 +96,7 @@ function AccessibilityAccordionItem({
iconName={IconName.Accessibility} iconName={IconName.Accessibility}
className={styles.accordionItem} className={styles.accordionItem}
variant="sidepeek" variant="sidepeek"
onOpen={() => trackAccordionClick("amenities:accessibility")} onOpen={() => tracking.trackAccordionItemOpen("amenities:accessibility")}
> >
<div className={styles.accessibilityContent}> <div className={styles.accessibilityContent}>
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">

View File

@@ -6,8 +6,7 @@ import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled" import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled"
import { trackOpenSidePeekEvent } from "@/utils/tracking" import { useTrackingContext } from "../../trackingContext"
import { HotelSidePeekContent } from "./HotelSidePeekContent" import { HotelSidePeekContent } from "./HotelSidePeekContent"
import type { import type {
@@ -16,7 +15,9 @@ import type {
Restaurant, Restaurant,
} from "@scandic-hotels/trpc/types/hotel" } from "@scandic-hotels/trpc/types/hotel"
import { SidePeekEnum } from "@/types/sidepeek" enum SidePeekEnum {
hotelDetails = "hotel-detail-side-peek",
}
interface HotelDetailsSidePeekProps { interface HotelDetailsSidePeekProps {
hotel: Hotel & { url: string | null } hotel: Hotel & { url: string | null }
@@ -48,7 +49,7 @@ const buttonPropsMap: Record<
}, },
} }
export default function HotelDetailsSidePeek({ export function HotelDetailsSidePeek({
hotel, hotel,
restaurants, restaurants,
additionalHotelData, additionalHotelData,
@@ -56,6 +57,7 @@ export default function HotelDetailsSidePeek({
wrapping = true, wrapping = true,
buttonVariant, buttonVariant,
}: HotelDetailsSidePeekProps) { }: HotelDetailsSidePeekProps) {
const tracking = useTrackingContext()
const buttonProps = buttonPropsMap[buttonVariant] const buttonProps = buttonPropsMap[buttonVariant]
return ( return (
@@ -64,7 +66,7 @@ export default function HotelDetailsSidePeek({
{...buttonProps} {...buttonProps}
wrapping={wrapping} wrapping={wrapping}
onPress={() => onPress={() =>
trackOpenSidePeekEvent({ tracking.trackOpenSidePeek({
name: SidePeekEnum.hotelDetails, name: SidePeekEnum.hotelDetails,
hotelId: hotel.operaId, hotelId: hotel.operaId,
includePathname: true, includePathname: true,

View File

@@ -1,6 +1,5 @@
"use client" "use client"
import { useSession } from "next-auth/react"
import { useState } from "react" import { useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
@@ -17,12 +16,12 @@ import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton
import Subtitle from "@scandic-hotels/design-system/Subtitle" import Subtitle from "@scandic-hotels/design-system/Subtitle"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import useLang from "@/hooks/useLang" import { useIsLoggedIn } from "../../hooks/useIsLoggedIn"
import { isValidClientSession } from "@/utils/clientSession" import useLang from "../../hooks/useLang"
import styles from "./listingHotelCardDialog.module.css" import styles from "./listingHotelCardDialog.module.css"
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" import type { HotelPin } from "../HotelCardDialogListing/utils"
interface ListingHotelCardProps { interface ListingHotelCardProps {
data: HotelPin data: HotelPin
@@ -38,8 +37,7 @@ export default function ListingHotelCardDialog({
const [imageError, setImageError] = useState(false) const [imageError, setImageError] = useState(false)
const { data: session } = useSession() const isUserLoggedIn = useIsLoggedIn()
const isUserLoggedIn = isValidClientSession(session)
const { const {
bookingCode, bookingCode,
name, name,
@@ -130,7 +128,6 @@ export default function ListingHotelCardDialog({
<Subtitle type="two"> <Subtitle type="two">
{publicPrice} {currency} {publicPrice} {currency}
</Subtitle> </Subtitle>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{memberPrice && <Caption>/</Caption>} {memberPrice && <Caption>/</Caption>}
</> </>
) : ( ) : (
@@ -169,8 +166,7 @@ export default function ListingHotelCardDialog({
} }
)} )}
{chequePrice.additionalPricePerStay > 0 {chequePrice.additionalPricePerStay > 0
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx ? " + " +
" + " +
intl.formatMessage( intl.formatMessage(
{ {
defaultMessage: "{price} {currency}", defaultMessage: "{price} {currency}",
@@ -182,7 +178,6 @@ export default function ListingHotelCardDialog({
) )
: null} : null}
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span> <span>
/ /
{intl.formatMessage({ {intl.formatMessage({
@@ -204,7 +199,6 @@ export default function ListingHotelCardDialog({
} }
)} )}
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span> <span>
/ /
{intl.formatMessage({ {intl.formatMessage({

View File

@@ -20,20 +20,25 @@ import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { useHotelFilterStore } from "@/stores/hotel-filters" import useInitializeFiltersFromUrl from "../../../../hooks/useInitializeFiltersFromUrl"
import { SortOrder } from "../../../../misc/sortOrder"
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl" import { useHotelFilterStore } from "../../../../stores/hotel-filters"
import { DEFAULT_SORT } from "../../HotelSorter" import { DEFAULT_SORT } from "../../HotelSorter"
import FilterContent from "../FilterContent" import FilterContent from "../FilterContent"
import styles from "./filterAndSortModal.module.css" import styles from "./filterAndSortModal.module.css"
import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal" import type { CategorizedHotelFilters } from "../../../../types"
import {
type SortItem, type SortItem = {
SortOrder, label: string
} from "@/types/components/hotelReservation/selectHotel/hotelSorter" value: string
}
type FilterAndSortModalProps = {
filters: CategorizedHotelFilters
setShowSkeleton?: (showSkeleton: boolean) => void
}
export default function FilterAndSortModal({ export default function FilterAndSortModal({
filters, filters,

View File

@@ -7,7 +7,13 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./filterCheckbox.module.css" import styles from "./filterCheckbox.module.css"
import type { FilterCheckboxProps } from "@/types/components/hotelReservation/selectHotel/filterCheckbox" type FilterCheckboxProps = {
name: string
id: string
isDisabled?: boolean
isSelected: boolean
onChange: (filterId: string) => void
}
export default function FilterCheckbox({ export default function FilterCheckbox({
isSelected, isSelected,

View File

@@ -8,10 +8,7 @@ import FilterCheckbox from "./FilterCheckbox"
import styles from "./filterContent.module.css" import styles from "./filterContent.module.css"
import type { import type { CategorizedHotelFilters, HotelFilter } from "../../../../types"
CategorizedHotelFilters,
HotelFilter,
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
interface FilterContentProps { interface FilterContentProps {
filters: CategorizedHotelFilters filters: CategorizedHotelFilters
@@ -83,7 +80,6 @@ export default function FilterContent({
isDisabled={isDisabled} isDisabled={isDisabled}
/> />
{!isDisabled && ( {!isDisabled && (
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
<span>{`(${combinedFiltersCount > 0 ? combinedFiltersCount : filterCount})`}</span> <span>{`(${combinedFiltersCount > 0 ? combinedFiltersCount : filterCount})`}</span>
)} )}
</li> </li>

View File

@@ -3,17 +3,20 @@
import { usePathname, useSearchParams } from "next/navigation" import { usePathname, useSearchParams } from "next/navigation"
import { useCallback, useEffect } from "react" import { useCallback, useEffect } from "react"
import { trackEvent } from "@scandic-hotels/common/tracking/base" import useInitializeFiltersFromUrl from "../../../../hooks/useInitializeFiltersFromUrl"
import { useHotelFilterStore } from "../../../../stores/hotel-filters"
import { useHotelFilterStore } from "@/stores/hotel-filters" import { useTrackingContext } from "../../../../trackingContext"
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl"
import FilterContent from "../FilterContent" import FilterContent from "../FilterContent"
import type { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" import type { CategorizedHotelFilters } from "../../../../types"
type HotelFiltersProps = {
filters: CategorizedHotelFilters
className?: string
}
export default function HotelFilter({ className, filters }: HotelFiltersProps) { export default function HotelFilter({ className, filters }: HotelFiltersProps) {
const tracking = useTrackingContext()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const pathname = usePathname() const pathname = usePathname()
useInitializeFiltersFromUrl() useInitializeFiltersFromUrl()
@@ -41,13 +44,18 @@ export default function HotelFilter({ className, filters }: HotelFiltersProps) {
.map((id) => surroundingsMap.get(id)) .map((id) => surroundingsMap.get(id))
.join(",") .join(",")
trackEvent({ tracking.trackGenericEvent({
event: "filterUsed", event: "filterUsed",
filter: { filter: {
filtersUsed: `Filters values - hotelfacilities:${hotelFacilitiesFilter}|hotelsurroundings:${hotelSurroundingsFilter}`, filtersUsed: `Filters values - hotelfacilities:${hotelFacilitiesFilter}|hotelsurroundings:${hotelSurroundingsFilter}`,
}, },
}) })
}, [activeFilters, filters.facilityFilters, filters.surroundingsFilters]) }, [
tracking,
activeFilters,
filters.facilityFilters,
filters.surroundingsFilters,
])
// Update the URL when the filters changes // Update the URL when the filters changes
useEffect(() => { useEffect(() => {

View File

@@ -4,7 +4,7 @@ import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { useHotelFilterStore } from "@/stores/hotel-filters" import { useHotelFilterStore } from "../../../stores/hotel-filters"
export default function HotelCount() { export default function HotelCount() {
const intl = useIntl() const intl = useIntl()

Some files were not shown because too many files have changed in this diff Show More