Feat/BOOK-426 campaign tag select hotel * fix(BOOK-426): do not show campaign tag if a regular booking code is used and the rate is a campaign * fix(BOOK-426): if no availability show booking code striketrough as default Approved-by: Erik Tiekstra
269 lines
9.3 KiB
TypeScript
269 lines
9.3 KiB
TypeScript
"use client"
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { useEffect, useMemo, useRef } from "react"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
|
import {
|
|
alternativeHotelsMap,
|
|
selectHotelMap,
|
|
} from "@scandic-hotels/common/constants/routes/hotelReservation"
|
|
import { useScrollToTop } from "@scandic-hotels/common/hooks/useScrollToTop"
|
|
import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton"
|
|
import { HotelCard } from "@scandic-hotels/design-system/HotelCard"
|
|
|
|
import { useGetPointsCurrency } from "../../bookingFlowConfig/bookingFlowConfigContext"
|
|
import { useIsLoggedIn } from "../../hooks/useIsLoggedIn"
|
|
import useLang from "../../hooks/useLang"
|
|
import { mapApiImagesToGalleryImages } from "../../misc/imageGallery"
|
|
import {
|
|
BookingCodeFilterEnum,
|
|
useBookingCodeFilterStore,
|
|
} 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 { getSortedHotels } from "./utils"
|
|
|
|
import styles from "./hotelCardListing.module.css"
|
|
|
|
import type { HotelType } from "@scandic-hotels/common/constants/hotelType"
|
|
|
|
import type { HotelResponse } from "../SelectHotel/helpers"
|
|
|
|
export enum HotelCardListingTypeEnum {
|
|
MapListing = "mapListing",
|
|
PageListing = "pageListing",
|
|
}
|
|
|
|
type HotelCardListingProps = {
|
|
hotelData: HotelResponse[]
|
|
type?: HotelCardListingTypeEnum
|
|
isAlternative?: boolean
|
|
}
|
|
|
|
export default function HotelCardListing({
|
|
hotelData,
|
|
type = HotelCardListingTypeEnum.PageListing,
|
|
isAlternative,
|
|
}: HotelCardListingProps) {
|
|
const router = useRouter()
|
|
const lang = useLang()
|
|
const intl = useIntl()
|
|
const isUserLoggedIn = useIsLoggedIn()
|
|
const searchParams = useSearchParams()
|
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
|
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
|
const { activeHotel, activate, disengage, engage } = useHotelsMapStore()
|
|
const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 })
|
|
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
|
const pointsCurrency = useGetPointsCurrency()
|
|
|
|
const sortBy = searchParams.get("sort") ?? DEFAULT_SORT
|
|
|
|
const bookingCode = searchParams.get("bookingCode")
|
|
// Special rates (corporate cheque, voucher) will not show regular rate hotels availability
|
|
const isSpecialRate = bookingCode
|
|
? hotelData.find(
|
|
(hotel) =>
|
|
hotel.availability.productType?.bonusCheque ||
|
|
hotel.availability.productType?.voucher
|
|
)
|
|
: false
|
|
const activeCodeFilter = useBookingCodeFilterStore(
|
|
(state) => state.activeCodeFilter
|
|
)
|
|
const isBookingCodeRateAvailable =
|
|
bookingCode && !isSpecialRate
|
|
? hotelData.some((hotel) => hotel.availability.bookingCode)
|
|
: false
|
|
const showOnlyBookingCodeRates =
|
|
isBookingCodeRateAvailable &&
|
|
activeCodeFilter === BookingCodeFilterEnum.Discounted
|
|
|
|
const hotelsWithBookingCode = hotelData.filter(
|
|
(hotel) => hotel.availability?.bookingCode
|
|
)
|
|
const isCampaignWithBookingCode =
|
|
!!bookingCode &&
|
|
hotelsWithBookingCode.length > 0 &&
|
|
hotelsWithBookingCode.every(
|
|
(hotel) =>
|
|
hotel.availability.productType?.public?.rateType ===
|
|
RateTypeEnum.PublicPromotion ||
|
|
hotel.availability.productType?.member?.rateType ===
|
|
RateTypeEnum.PublicPromotion
|
|
)
|
|
|
|
const unfilteredHotelCount = showOnlyBookingCodeRates
|
|
? hotelData.filter((hotel) => hotel.availability.bookingCode).length
|
|
: hotelData.length
|
|
|
|
const hotels = useMemo(() => {
|
|
const sortedHotels = getSortedHotels({
|
|
hotels: hotelData,
|
|
sortBy,
|
|
bookingCode: isSpecialRate ? null : bookingCode,
|
|
})
|
|
|
|
const updatedHotelsList = showOnlyBookingCodeRates
|
|
? sortedHotels.filter((hotel) => hotel.availability.bookingCode)
|
|
: sortedHotels
|
|
|
|
if (!activeFilters.length) {
|
|
return updatedHotelsList
|
|
}
|
|
|
|
return updatedHotelsList.filter((hotel) =>
|
|
activeFilters.every((appliedFilterId) =>
|
|
hotel.hotel.detailedFacilities.some(
|
|
(facility) => facility.id.toString() === appliedFilterId
|
|
)
|
|
)
|
|
)
|
|
}, [
|
|
activeFilters,
|
|
bookingCode,
|
|
hotelData,
|
|
sortBy,
|
|
showOnlyBookingCodeRates,
|
|
isSpecialRate,
|
|
])
|
|
|
|
useEffect(() => {
|
|
if (activeCardRef.current && type === HotelCardListingTypeEnum.MapListing) {
|
|
activeCardRef.current.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "nearest",
|
|
inline: "center",
|
|
})
|
|
}
|
|
}, [activeHotel, type])
|
|
|
|
useEffect(() => {
|
|
if (type === HotelCardListingTypeEnum.PageListing) {
|
|
setResultCount(hotels.length, unfilteredHotelCount)
|
|
}
|
|
}, [hotels, setResultCount, type, unfilteredHotelCount])
|
|
|
|
function isHotelActiveInMapView(hotelName: string): boolean {
|
|
return (
|
|
hotelName === activeHotel && type === HotelCardListingTypeEnum.MapListing
|
|
)
|
|
}
|
|
|
|
return (
|
|
<section className={styles.hotelCards}>
|
|
{hotels.map((hotel) => (
|
|
<div
|
|
key={hotel.hotel.operaId}
|
|
ref={isHotelActiveInMapView(hotel.hotel.name) ? activeCardRef : null}
|
|
data-active={
|
|
isHotelActiveInMapView(hotel.hotel.name) ? "true" : "false"
|
|
}
|
|
>
|
|
<HotelCard
|
|
hotel={{
|
|
id: hotel.hotel.operaId,
|
|
name: hotel.hotel.name,
|
|
address: hotel.hotel.address,
|
|
description: hotel.hotel.hotelContent.texts.descriptions?.short,
|
|
hotelType: hotel.hotel.hotelType as HotelType,
|
|
detailedFacilities: hotel.hotel.detailedFacilities,
|
|
ratings: {
|
|
tripAdvisor: hotel.hotel.ratings?.tripAdvisor.rating,
|
|
},
|
|
}}
|
|
pointsCurrency={pointsCurrency}
|
|
lang={lang}
|
|
fullPrice={!hotel.availability.bookingCode}
|
|
prices={
|
|
hotel.availability.productType && {
|
|
public: hotel.availability.productType?.public
|
|
? {
|
|
...hotel.availability.productType.public,
|
|
requestedPrice:
|
|
hotel.availability.productType?.public.requestedPrice ??
|
|
undefined,
|
|
}
|
|
: undefined,
|
|
member: hotel.availability.productType?.member
|
|
? {
|
|
...hotel.availability.productType.member,
|
|
requestedPrice:
|
|
hotel.availability.productType?.member.requestedPrice ??
|
|
undefined,
|
|
}
|
|
: undefined,
|
|
voucher: hotel.availability.productType?.voucher,
|
|
bonusCheque: hotel.availability.productType?.bonusCheque
|
|
? {
|
|
...hotel.availability.productType.bonusCheque,
|
|
requestedPrice:
|
|
hotel.availability.productType.bonusCheque
|
|
.requestedPrice ?? undefined,
|
|
}
|
|
: undefined,
|
|
redemptions: hotel.availability.productType?.redemptions?.map(
|
|
(redemption) => ({
|
|
...redemption,
|
|
localPrice: {
|
|
...redemption.localPrice,
|
|
currency: redemption.localPrice.currency,
|
|
},
|
|
})
|
|
),
|
|
}
|
|
}
|
|
onHover={() => engage(hotel.hotel.name)}
|
|
onHoverEnd={() => disengage()}
|
|
onAddressClick={() => {
|
|
const mapUrl = isAlternative
|
|
? alternativeHotelsMap(lang)
|
|
: selectHotelMap(lang)
|
|
|
|
disengage() // Disengage the current hotel to avoid the hover state from being active when clicking on the address
|
|
activate(hotel.hotel.name)
|
|
router.push(`${mapUrl}?${searchParams.toString()}`)
|
|
}}
|
|
belowInfoSlot={
|
|
<HotelDetailsSidePeek
|
|
hotel={{ ...hotel.hotel, url: hotel.url }}
|
|
restaurants={hotel.restaurants}
|
|
additionalHotelData={hotel.additionalData}
|
|
triggerLabel={intl.formatMessage({
|
|
id: "destination.seeHotelDetails",
|
|
defaultMessage: "See hotel details",
|
|
})}
|
|
buttonVariant="primary"
|
|
/>
|
|
}
|
|
distanceToCityCenter={hotel.hotel.location.distanceToCentre}
|
|
images={mapApiImagesToGalleryImages(hotel.hotel.galleryImages)}
|
|
isUserLoggedIn={isUserLoggedIn}
|
|
state={
|
|
isHotelActiveInMapView(hotel.hotel.name) ? "active" : "default"
|
|
}
|
|
type={type}
|
|
bookingCode={bookingCode}
|
|
isCampaignWithBookingCode={isCampaignWithBookingCode}
|
|
isAlternative={isAlternative}
|
|
/>
|
|
</div>
|
|
))}
|
|
{showBackToTop && (
|
|
<BackToTopButton
|
|
position="right"
|
|
onClick={scrollToTop}
|
|
label={intl.formatMessage({
|
|
id: "common.backToTop",
|
|
defaultMessage: "Back to top",
|
|
})}
|
|
/>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|