Files
web/apps/scandic-web/components/HotelReservation/HotelCardListing/index.tsx
Tobias Johansson 9a9789e736 Merged in feat/SW-1549-map-improvements (pull request #1783)
Feat/SW-1549 map improvements

* fix: imported new icon

* refactor: rename component and set map handling to 'greedy'

* fix: show cards for 3s after hover

* refactor: update styles and added HotelPin component

* fix: change from close to back icon

* refactor: update to only use 1 state value for active pin and card

* fix: add click handler when dialog is opened

* fix: performance fixes for the dialog carousel

* fix: added border

* fix: clear timeout on mouseenter

* fix: changed to absolute import

* fix: moved hover state into the store

* fix: renamed store actions


Approved-by: Michael Zetterberg
2025-04-15 13:23:23 +00:00

168 lines
5.5 KiB
TypeScript

"use client"
import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { useEffect, useMemo, useRef } from "react"
import { useIntl } from "react-intl"
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
import { useHotelFilterStore } from "@/stores/hotel-filters"
import { useHotelsMapStore } from "@/stores/hotels-map"
import Alert from "@/components/TempDesignSystem/Alert"
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
import { useScrollToTop } from "@/hooks/useScrollToTop"
import { isValidClientSession } from "@/utils/clientSession"
import HotelCard from "../HotelCard"
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
import { getSortedHotels } from "./utils"
import styles from "./hotelCardListing.module.css"
import {
type HotelCardListingProps,
HotelCardListingTypeEnum,
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import { AlertTypeEnum } from "@/types/enums/alert"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function HotelCardListing({
hotelData,
type = HotelCardListingTypeEnum.PageListing,
}: HotelCardListingProps) {
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const searchParams = useSearchParams()
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
const intl = useIntl()
const { activeHotel } = useHotelsMapStore()
const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 })
const activeCardRef = useRef<HTMLDivElement | null>(null)
const sortBy = searchParams.get("sort") ?? DEFAULT_SORT
const bookingCode = searchParams.get("bookingCode")
// Special rates (corporate cheque, voucher and reward nights) will not have regular rate hotels availability
const isSpecialRate = hotelData.find(
(hotel) =>
hotel.availability.productType?.bonusCheque ||
hotel.availability.productType?.voucher ||
hotel.availability.productType?.redemptions
)
const activeCodeFilter = useBookingCodeFilterStore(
(state) => state.activeCodeFilter
)
const hotels = useMemo(() => {
const sortedHotels = getSortedHotels({
hotels: hotelData,
sortBy,
bookingCode: isSpecialRate ? null : bookingCode,
})
const updatedHotelsList =
bookingCode && !isSpecialRate
? sortedHotels.filter(
(hotel) =>
!hotel.availability.productType ||
activeCodeFilter === BookingCodeFilterEnum.All ||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
hotel.availability.productType.public?.rateType !==
RateTypeEnum.Regular &&
hotel.availability.productType.member?.rateType !==
RateTypeEnum.Regular) ||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
(hotel.availability.productType.public?.rateType ===
RateTypeEnum.Regular ||
hotel.availability.productType.member?.rateType ===
RateTypeEnum.Regular))
)
: sortedHotels
if (!activeFilters.length) {
return updatedHotelsList
}
return updatedHotelsList.filter((hotel) =>
activeFilters.every((appliedFilterId) =>
hotel.hotel.detailedFacilities.some(
(facility) => facility.id.toString() === appliedFilterId
)
)
)
}, [
activeCodeFilter,
activeFilters,
bookingCode,
hotelData,
sortBy,
isSpecialRate,
])
useEffect(() => {
if (activeCardRef.current && type === HotelCardListingTypeEnum.MapListing) {
activeCardRef.current.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
})
}
}, [activeHotel, type])
useEffect(() => {
setResultCount(hotels.length)
}, [hotels, setResultCount])
function isHotelActiveInMapView(hotelName: string): boolean {
return (
hotelName === activeHotel && type === HotelCardListingTypeEnum.MapListing
)
}
return (
<section className={styles.hotelCards}>
{hotels?.length
? hotels.map((hotel) => (
<div
key={hotel.hotel.operaId}
ref={
isHotelActiveInMapView(hotel.hotel.name) ? activeCardRef : null
}
data-active={
isHotelActiveInMapView(hotel.hotel.name) ? "true" : "false"
}
>
<HotelCard
hotelData={hotel}
isUserLoggedIn={isUserLoggedIn}
state={
isHotelActiveInMapView(hotel.hotel.name)
? "active"
: "default"
}
type={type}
bookingCode={bookingCode}
/>
</div>
))
: null}
{!hotels?.length && activeFilters ? (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "No hotels match your filters",
})}
text={intl.formatMessage({
defaultMessage:
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
})}
/>
) : null}
{showBackToTop && (
<BackToTopButton position="right" onClick={scrollToTop} />
)}
</section>
)
}