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
143 lines
3.9 KiB
TypeScript
143 lines
3.9 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useEffect, useRef } from "react"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
|
|
|
import HotelCardDialog from "../HotelCardDialog"
|
|
import { getHotelPins } from "./utils"
|
|
|
|
import styles from "./hotelCardDialogListing.module.css"
|
|
|
|
import type { HotelCardDialogListingProps } from "@/types/components/hotelReservation/selectHotel/map"
|
|
|
|
export default function HotelCardDialogListing({
|
|
hotels,
|
|
}: HotelCardDialogListingProps) {
|
|
const intl = useIntl()
|
|
const isRedemption = hotels?.find(
|
|
(hotel) => hotel.availability.productType?.redemptions?.length
|
|
)
|
|
const currencyValue = isRedemption
|
|
? intl.formatMessage({
|
|
defaultMessage: "Points",
|
|
})
|
|
: undefined
|
|
const hotelsPinData = getHotelPins(hotels, currencyValue)
|
|
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
|
const observerRef = useRef<IntersectionObserver | null>(null)
|
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
const isScrollingRef = useRef<boolean>(false)
|
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const { activeHotel, activate, deactivate } = useHotelsMapStore()
|
|
|
|
const handleIntersection = useCallback(
|
|
(entries: IntersectionObserverEntry[]) => {
|
|
// skip intersection handling during scrolling
|
|
if (isScrollingRef.current) return
|
|
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current)
|
|
}
|
|
|
|
debounceTimerRef.current = setTimeout(() => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
const cardName = entry.target.getAttribute("data-name")
|
|
if (cardName && cardName !== activeHotel) {
|
|
activate(cardName)
|
|
}
|
|
}
|
|
})
|
|
}, 100)
|
|
},
|
|
[activate, activeHotel]
|
|
)
|
|
|
|
useEffect(() => {
|
|
observerRef.current = new IntersectionObserver(handleIntersection, {
|
|
root: null,
|
|
threshold: [0.3, 0.5, 0.7],
|
|
})
|
|
|
|
const elements = document.querySelectorAll("[data-name]")
|
|
elements.forEach((el) => observerRef.current?.observe(el))
|
|
|
|
return () => {
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current)
|
|
}
|
|
|
|
elements.forEach((el) => observerRef.current?.unobserve(el))
|
|
observerRef.current?.disconnect()
|
|
observerRef.current = null
|
|
}
|
|
}, [handleIntersection])
|
|
|
|
useEffect(() => {
|
|
if (activeCardRef.current) {
|
|
isScrollingRef.current = true
|
|
|
|
requestAnimationFrame(() => {
|
|
activeCardRef.current?.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "nearest",
|
|
inline: "center",
|
|
})
|
|
|
|
setTimeout(() => {
|
|
isScrollingRef.current = false
|
|
}, 800)
|
|
})
|
|
}
|
|
}, [activeHotel])
|
|
|
|
useEffect(() => {
|
|
const handleMapClick = (e: MouseEvent) => {
|
|
// ignore clicks within the dialog
|
|
if (dialogRef.current?.contains(e.target as Node)) {
|
|
return
|
|
}
|
|
|
|
// ignore clicks on hotel pins
|
|
const target = e.target as HTMLElement
|
|
if (target.closest("[data-hotelpin]")) {
|
|
return
|
|
}
|
|
|
|
deactivate()
|
|
}
|
|
|
|
if (activeHotel) {
|
|
document.addEventListener("click", handleMapClick)
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener("click", handleMapClick)
|
|
}
|
|
}, [dialogRef, activeHotel, deactivate])
|
|
|
|
return (
|
|
<div className={styles.hotelCardDialogListing} ref={dialogRef}>
|
|
{hotelsPinData?.map((data) => {
|
|
const isActive = data.name === activeHotel
|
|
return (
|
|
<div
|
|
key={data.name}
|
|
ref={isActive ? activeCardRef : null}
|
|
data-name={data.name}
|
|
>
|
|
<HotelCardDialog
|
|
data={data}
|
|
isOpen={!!activeHotel}
|
|
handleClose={deactivate}
|
|
type="listing"
|
|
/>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|