From c3e56d5b496df08e1bf1a0b4a425b09fb96ac141 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 12 Dec 2024 10:04:16 +0100 Subject: [PATCH] fix(SW-1168) Show only hotel cards that are visible on map --- .../SelectHotelMapContent/index.tsx | 169 ++++++++++++++++++ .../selectHotelMapContent.module.css} | 1 + .../SelectHotelMapContent/utils.ts | 29 +++ .../SelectHotel/SelectHotelMap/index.tsx | 123 +------------ components/Maps/InteractiveMap/index.tsx | 3 +- .../hotelPage/map/interactiveMap.ts | 5 +- 6 files changed, 211 insertions(+), 119 deletions(-) create mode 100644 components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx rename components/HotelReservation/SelectHotel/SelectHotelMap/{selectHotelMap.module.css => SelectHotelMapContent/selectHotelMapContent.module.css} (98%) create mode 100644 components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx new file mode 100644 index 000000000..ea9fc8212 --- /dev/null +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -0,0 +1,169 @@ +import { useMap } from "@vis.gl/react-google-maps" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" + +import { selectHotel } from "@/constants/routes/hotelReservation" +import { useHotelFilterStore } from "@/stores/hotel-filters" +import { useHotelsMapStore } from "@/stores/hotels-map" + +import { RoomCardSkeleton } from "@/components/HotelReservation/SelectRate/RoomSelection/RoomCard/RoomCardSkeleton" +import { CloseIcon, CloseLargeIcon } from "@/components/Icons" +import InteractiveMap from "@/components/Maps/InteractiveMap" +import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import useLang from "@/hooks/useLang" +import { debounce } from "@/utils/debounce" + +import FilterAndSortModal from "../../FilterAndSortModal" +import HotelListing from "../HotelListing" +import { getVisibleHotels } from "./utils" + +import styles from "./selectHotelMapContent.module.css" + +import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map" + +export default function SelectHotelContent({ + hotelPins, + cityCoordinates, + mapId, + hotels, + filterList, +}: Omit) { + const lang = useLang() + const intl = useIntl() + const map = useMap() + + const isAboveMobile = useMediaQuery("(min-width: 768px)") + const [visibleHotels, setVisibleHotels] = useState([]) + const [showBackToTop, setShowBackToTop] = useState(false) + const [isMapLoaded, setIsMapLoaded] = useState(false) + const listingContainerRef = useRef(null) + + const activeFilters = useHotelFilterStore((state) => state.activeFilters) + const { activeHotelCard, activeHotelPin } = useHotelsMapStore() + + const coordinates = useMemo( + () => + isAboveMobile + ? cityCoordinates + : { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }, + [isAboveMobile, cityCoordinates] + ) + + useEffect(() => { + if (listingContainerRef.current) { + const activeElement = + listingContainerRef.current.querySelector(`[data-active="true"]`) + if (activeElement) { + activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" }) + } + } + }, [activeHotelCard, activeHotelPin]) + + useEffect(() => { + const hotelListingElement = document.querySelector( + `.${styles.listingContainer}` + ) + if (!hotelListingElement) return + + const handleScroll = () => { + const hasScrolledPast = hotelListingElement.scrollTop > 490 + setShowBackToTop(hasScrolledPast) + } + + hotelListingElement.addEventListener("scroll", handleScroll) + return () => hotelListingElement.removeEventListener("scroll", handleScroll) + }, []) + + function scrollToTop() { + const hotelListingElement = document.querySelector( + `.${styles.listingContainer}` + ) + hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" }) + } + + const filteredHotelPins = useMemo( + () => + hotelPins.filter((hotel) => + activeFilters.every((filterId) => + hotel.facilityIds.includes(Number(filterId)) + ) + ), + [activeFilters, hotelPins] + ) + + const getHotelCards = useCallback(() => { + const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map) + setVisibleHotels(visibleHotels) + setTimeout(() => { + setIsMapLoaded(true) + }, 750) + }, [hotels, filteredHotelPins, map]) + + const debouncedUpdateHotelCards = useMemo( + () => + debounce(() => { + if (!map) return + setIsMapLoaded(false) + getHotelCards() + }, 100), + [map, getHotelCards] + ) + + const closeButton = ( + + ) + + return ( +
+
+
+ + +
+ {isMapLoaded ? ( + + ) : ( + <> + + + + )} + {showBackToTop && ( + + )} +
+ +
+ ) +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/selectHotelMap.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css similarity index 98% rename from components/HotelReservation/SelectHotel/SelectHotelMap/selectHotelMap.module.css rename to components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css index 9017dff96..811f94d68 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/selectHotelMap.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css @@ -36,6 +36,7 @@ padding: var(--Spacing-x3) var(--Spacing-x4); overflow-y: auto; min-width: 420px; + width: 420px; position: relative; } .container { diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts new file mode 100644 index 000000000..481ad21ba --- /dev/null +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts @@ -0,0 +1,29 @@ +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" + +export function getVisibleHotelPins( + map: google.maps.Map | null, + filteredHotelPins: HotelPin[] +) { + if (!map || !filteredHotelPins) return [] + + const bounds = map.getBounds() + if (!bounds) return [] + + return filteredHotelPins.filter((pin: any) => { + const { lat, lng } = pin.coordinates + return bounds.contains({ lat, lng }) + }) +} + +export function getVisibleHotels( + hotels: HotelData[], + filteredHotelPins: HotelPin[], + map: google.maps.Map | null +) { + const visibleHotelPins = getVisibleHotelPins(map, filteredHotelPins) + const visibleHotels = hotels.filter((hotel: any) => + visibleHotelPins.some((pin: any) => pin.operaId === hotel.hotelData.operaId) + ) + return visibleHotels +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 3712af0e6..a598050b1 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -1,25 +1,8 @@ "use client" import { APIProvider } from "@vis.gl/react-google-maps" -import { useEffect, useMemo, useRef, useState } from "react" -import { useIntl } from "react-intl" -import { useMediaQuery } from "usehooks-ts" -import { selectHotel } from "@/constants/routes/hotelReservation" -import { useHotelFilterStore } from "@/stores/hotel-filters" -import { useHotelsMapStore } from "@/stores/hotels-map" - -import { CloseIcon, CloseLargeIcon } from "@/components/Icons" -import InteractiveMap from "@/components/Maps/InteractiveMap" -import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" -import Button from "@/components/TempDesignSystem/Button" -import Link from "@/components/TempDesignSystem/Link" -import useLang from "@/hooks/useLang" - -import FilterAndSortModal from "../FilterAndSortModal" -import HotelListing from "./HotelListing" - -import styles from "./selectHotelMap.module.css" +import SelectHotelContent from "./SelectHotelMapContent" import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map" @@ -31,105 +14,15 @@ export default function SelectHotelMap({ filterList, cityCoordinates, }: SelectHotelMapProps) { - const lang = useLang() - const intl = useIntl() - const isAboveMobile = useMediaQuery("(min-width: 768px)") - const [showBackToTop, setShowBackToTop] = useState(false) - const listingContainerRef = useRef(null) - const activeFilters = useHotelFilterStore((state) => state.activeFilters) - const { activeHotelCard, activeHotelPin } = useHotelsMapStore() - - const coordinates = isAboveMobile - ? cityCoordinates - : { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 } - - useEffect(() => { - if (listingContainerRef.current) { - const activeElement = - listingContainerRef.current.querySelector(`[data-active="true"]`) - if (activeElement) { - activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" }) - } - } - }, [activeHotelCard, activeHotelPin]) - - useEffect(() => { - const hotelListingElement = document.querySelector( - `.${styles.listingContainer}` - ) - if (!hotelListingElement) return - - const handleScroll = () => { - const hasScrolledPast = hotelListingElement.scrollTop > 490 - setShowBackToTop(hasScrolledPast) - } - - hotelListingElement.addEventListener("scroll", handleScroll) - return () => hotelListingElement.removeEventListener("scroll", handleScroll) - }, []) - - function scrollToTop() { - const hotelListingElement = document.querySelector( - `.${styles.listingContainer}` - ) - hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" }) - } - - const filteredHotelPins = useMemo( - () => - hotelPins.filter((hotel) => - activeFilters.every((filterId) => - hotel.facilityIds.includes(Number(filterId)) - ) - ), - [activeFilters, hotelPins] - ) - - const closeButton = ( - - ) return ( -
-
-
- - -
- - {showBackToTop && ( - - )} -
- -
+
) } diff --git a/components/Maps/InteractiveMap/index.tsx b/components/Maps/InteractiveMap/index.tsx index 25fc1aabf..4e7f142c1 100644 --- a/components/Maps/InteractiveMap/index.tsx +++ b/components/Maps/InteractiveMap/index.tsx @@ -19,6 +19,7 @@ export default function InteractiveMap({ hotelPins, mapId, closeButton, + onMapLoad, onActivePoiChange, }: InteractiveMapProps) { const intl = useIntl() @@ -47,7 +48,7 @@ export default function InteractiveMap({ return (
- + {hotelPins && } {pointsOfInterest && ( void onActivePoiChange?: (poi: PointOfInterest["name"] | null) => void }