Files
web/packages/booking-flow/lib/components/HotelCardListing/index.tsx
Anton Gunnarsson b2398dba4a Merged in feat/sw-3587-add-partner-copy-for-member-price (pull request #3053)
feat(SW-3587): Add new member price copy to partner variants

* Add new member price copy to partner variants


Approved-by: Joakim Jäderberg
2025-11-03 07:57:23 +00:00

274 lines
9.4 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 {
useBookingFlowConfig,
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 config = useBookingFlowConfig()
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}
isPartnerBrand={config.variant !== "scandic"}
/>
</div>
))}
{showBackToTop && (
<BackToTopButton
position="right"
onClick={scrollToTop}
label={intl.formatMessage({
id: "common.backToTop",
defaultMessage: "Back to top",
})}
/>
)}
</section>
)
}