This creates the alternative hotels page. It is mostly a copy of the select hotel page, and most of the contents of the pages lives under the same component in /components.

Merged in feat/sw-397-alternative-hotels (pull request #1211)

Feat/sw 397 alternative hotels

* fix(SW-397): create alternative hotels page

* update types

* Adapt to new changes for fetching data

* Make bookingcode optional

* Code review fixes


Approved-by: Simon.Emanuelsson
This commit is contained in:
Niclas Edenvin
2025-01-28 12:08:40 +00:00
parent 4247e37667
commit ef22fc4627
28 changed files with 693 additions and 105 deletions

View File

@@ -77,7 +77,7 @@ export default function BookingWidgetClient({
const selectedLocation = bookingWidgetSearchData
? getLocationObj(
(bookingWidgetSearchData.hotel ??
(bookingWidgetSearchData.hotelId ??
bookingWidgetSearchData.city) as string
)
: undefined

View File

@@ -2,7 +2,10 @@
import { useIntl } from "react-intl"
import { selectHotelMap } from "@/constants/routes/hotelReservation"
import {
alternativeHotelsMap,
selectHotelMap,
} from "@/constants/routes/hotelReservation"
import { MapIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
@@ -17,8 +20,10 @@ import type { CategorizedFilters } from "@/types/components/hotelReservation/sel
export default function MobileMapButtonContainer({
filters,
isAlternative,
}: {
filters: CategorizedFilters
isAlternative?: boolean
}) {
const intl = useIntl()
const lang = useLang()
@@ -33,7 +38,9 @@ export default function MobileMapButtonContainer({
size="small"
>
<Link
href={selectHotelMap(lang)}
href={
isAlternative ? alternativeHotelsMap(lang) : selectHotelMap(lang)
}
color="baseButtonTextOnFillNormal"
keepSearchParams
weight="bold"

View File

@@ -0,0 +1,48 @@
import { alternativeHotels } from "@/constants/routes/hotelReservation"
import Alert from "@/components/TempDesignSystem/Alert"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import type { NoAvailabilityAlertProp } from "@/types/components/hotelReservation/selectHotel/noAvailabilityAlert"
import { AlertTypeEnum } from "@/types/enums/alert"
export default async function NoAvailabilityAlert({
hotels,
isAllUnavailable,
isAlternative,
}: NoAvailabilityAlertProp) {
const intl = await getIntl()
const lang = getLang()
if (!isAllUnavailable) {
return null
}
if (hotels.length === 1 && !isAlternative) {
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No availability" })}
text={intl.formatMessage({
id: "Please try and change your search for this destination or see alternative hotels.",
})}
link={{
title: intl.formatMessage({ id: "See alternative hotels" }),
url: `${alternativeHotels(lang)}?hotel=${hotels[0].hotelData.operaId}`,
keepSearchParams: true,
}}
/>
)
}
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No availability" })}
text={intl.formatMessage({
id: "There are no rooms available that match your request.",
})}
/>
)
}

View File

@@ -6,6 +6,7 @@ import { env } from "@/env/server"
import { getCityCoordinates } from "@/lib/trpc/memoizedRequests"
import {
fetchAlternativeHotels,
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
@@ -36,16 +37,20 @@ function isValidHotelData(hotel: NullableHotelData): hotel is HotelData {
export async function SelectHotelMapContainer({
searchParams,
isAlternativeHotels,
}: SelectHotelMapContainerProps) {
const lang = getLang()
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const getHotelSearchDetailsPromise = safeTry(
getHotelSearchDetails({
searchParams: searchParams as SelectHotelSearchParams & {
[key: string]: string
getHotelSearchDetails(
{
searchParams: searchParams as SelectHotelSearchParams & {
[key: string]: string
},
},
})
isAlternativeHotels
)
)
const [searchDetails] = await getHotelSearchDetailsPromise
@@ -58,19 +63,29 @@ export async function SelectHotelMapContainer({
adultsInRoom,
childrenInRoom,
childrenInRoomString,
hotel: isAlternativeFor,
} = searchDetails
if (!city) return notFound()
const fetchAvailableHotelsPromise = safeTry(
fetchAvailableHotels({
cityId: city.id,
roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom,
children: childrenInRoomString,
})
)
const fetchAvailableHotelsPromise = isAlternativeFor
? safeTry(
fetchAlternativeHotels(isAlternativeFor.id, {
roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom,
children: childrenInRoomString,
})
)
: safeTry(
fetchAvailableHotels({
cityId: city.id,
roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom,
children: childrenInRoomString,
})
)
const [hotels] = await fetchAvailableHotelsPromise
@@ -87,18 +102,24 @@ export async function SelectHotelMapContainer({
const departureDate = new Date(selectHotelParams.toDate)
const pageTrackingData: TrackingSDKPageData = {
pageId: "select-hotel",
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
domainLanguage: lang,
channel: TrackingChannelEnum["hotelreservation"],
pageName: "hotelreservation|select-hotel|mapview",
siteSections: "hotelreservation|select-hotel|mapview",
pageName: isAlternativeHotels
? "hotelreservation|alternative-hotels|mapview"
: "hotelreservation|select-hotel|mapview",
siteSections: isAlternativeHotels
? "hotelreservation|altervative-hotels|mapview"
: "hotelreservation|select-hotel|mapview",
pageType: "bookinghotelsmapviewpage",
siteVersion: "new-web",
}
const hotelsTrackingData: TrackingSDKHotelInfo = {
availableResults: validHotels.length,
searchTerm: selectHotelParams.city,
searchTerm: isAlternativeFor
? selectHotelParams.hotelId
: (selectHotelParams.city as string),
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adultsInRoom,

View File

@@ -3,18 +3,20 @@ import { notFound } from "next/navigation"
import { Suspense } from "react"
import {
alternativeHotels,
alternativeHotelsMap,
selectHotel,
selectHotelMap,
} from "@/constants/routes/hotelReservation"
import {
fetchAlternativeHotels,
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
import Alert from "@/components/TempDesignSystem/Alert"
import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
@@ -29,6 +31,7 @@ import HotelCount from "./HotelCount"
import HotelFilter from "./HotelFilter"
import HotelSorter from "./HotelSorter"
import MobileMapButtonContainer from "./MobileMapButtonContainer"
import NoAvailabilityAlert from "./NoAvailabilityAlert"
import styles from "./selectHotel.module.css"
@@ -41,20 +44,23 @@ import {
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { AlertTypeEnum } from "@/types/enums/alert"
export default async function SelectHotel({
params,
searchParams,
isAlternativeHotels,
}: SelectHotelProps) {
const intl = await getIntl()
const getHotelSearchDetailsPromise = safeTry(
getHotelSearchDetails({
searchParams: searchParams as SelectHotelSearchParams & {
[key: string]: string
getHotelSearchDetails(
{
searchParams: searchParams as SelectHotelSearchParams & {
[key: string]: string
},
},
})
isAlternativeHotels
)
)
const [searchDetails] = await getHotelSearchDetailsPromise
@@ -67,19 +73,29 @@ export default async function SelectHotel({
adultsInRoom,
childrenInRoomString,
childrenInRoom,
hotel: isAlternativeFor,
} = searchDetails
if (!city) return notFound()
const hotelsPromise = safeTry(
fetchAvailableHotels({
cityId: city.id,
roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom,
children: childrenInRoomString,
})
)
const hotelsPromise = isAlternativeFor
? safeTry(
fetchAlternativeHotels(isAlternativeFor.id, {
roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom,
children: childrenInRoomString,
})
)
: safeTry(
fetchAvailableHotels({
cityId: city.id,
roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom,
children: childrenInRoomString,
})
)
const [hotels] = await hotelsPromise
@@ -105,32 +121,50 @@ export default async function SelectHotel({
href: `/${params.lang}/hotelreservation`,
uid: "hotel-reservation",
},
{
title: intl.formatMessage({ id: "Select hotel" }),
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
uid: "select-hotel",
},
{
title: city.name,
uid: city.id,
},
isAlternativeFor
? {
title: intl.formatMessage({ id: "Alternative hotels" }),
href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`,
uid: "alternative-hotels",
}
: {
title: intl.formatMessage({ id: "Select hotel" }),
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
uid: "select-hotel",
},
isAlternativeFor
? {
title: isAlternativeFor.name,
uid: isAlternativeFor.id,
}
: {
title: city.name,
uid: city.id,
},
]
const isAllUnavailable = hotels?.every((hotel) => hotel.price === undefined)
const isAllUnavailable =
hotels?.every((hotel) => hotel.price === undefined) || false
const pageTrackingData: TrackingSDKPageData = {
pageId: "select-hotel",
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
domainLanguage: params.lang,
channel: TrackingChannelEnum["hotelreservation"],
pageName: "hotelreservation|select-hotel",
siteSections: "hotelreservation|select-hotel",
pageName: isAlternativeFor
? "hotelreservation|alternative-hotels"
: "hotelreservation|select-hotel",
siteSections: isAlternativeFor
? "hotelreservation|alternative-hotels"
: "hotelreservation|select-hotel",
pageType: "bookinghotelspage",
siteVersion: "new-web",
}
const hotelsTrackingData: TrackingSDKHotelInfo = {
availableResults: validHotels.length,
searchTerm: selectHotelParams.city,
searchTerm: isAlternativeFor
? selectHotelParams.hotelId
: (selectHotelParams.city as string),
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adultsInRoom,
@@ -155,7 +189,11 @@ export default async function SelectHotel({
<Breadcrumbs breadcrumbs={breadcrumbs} />
<div className={styles.title}>
<div className={styles.cityInformation}>
<Subtitle>{city.name}</Subtitle>
<Subtitle>
{isAlternativeFor
? `${intl.formatMessage({ id: "Alternatives for" })} ${isAlternativeFor.name}`
: city.name}
</Subtitle>
<HotelCount />
</div>
<div className={styles.sorter}>
@@ -171,18 +209,22 @@ export default async function SelectHotel({
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap(params.lang)}
href={
isAlternativeFor
? alternativeHotelsMap(params.lang)
: selectHotelMap(params.lang)
}
keepSearchParams
>
<div className={styles.mapContainer}>
<StaticMap
city={selectHotelParams.city}
city={city.name}
country={isCityWithCountry(city) ? city.country : undefined}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${selectHotelParams.city} city center`}
altText={`Map of ${city.name} city center`}
/>
<Button wrapping size="medium" intent="text" theme="base">
{intl.formatMessage({ id: "See map" })}
@@ -197,27 +239,23 @@ export default async function SelectHotel({
) : (
<div className={styles.mapContainer}>
<StaticMap
city={selectHotelParams.city}
city={city.name}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${selectHotelParams.city} city center`}
altText={`Map of ${city.name} city center`}
/>
</div>
)}
<HotelFilter filters={filterList} className={styles.filter} />
</div>
<div className={styles.hotelList}>
{isAllUnavailable && (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No availability" })}
text={intl.formatMessage({
id: "There are no rooms available that match your request.",
})}
/>
)}
<NoAvailabilityAlert
isAlternative={!!isAlternativeFor}
hotels={validHotels}
isAllUnavailable={isAllUnavailable}
/>
<HotelCardListing hotelData={validHotels} />
</div>
</main>

View File

@@ -19,5 +19,6 @@ export interface AlertProps extends VariantProps<typeof alertVariants> {
link?: {
url: string
title: string
keepSearchParams?: boolean
} | null
}

View File

@@ -64,7 +64,12 @@ export default function Alert({
) : null}
</div>
{link ? (
<Link color="burgundy" textDecoration="underline" href={link.url}>
<Link
color="burgundy"
textDecoration="underline"
href={link.url}
keepSearchParams={link.keepSearchParams}
>
{link.title}
</Link>
) : null}