From c3e56d5b496df08e1bf1a0b4a425b09fb96ac141 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 12 Dec 2024 10:04:16 +0100 Subject: [PATCH 1/3] 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 } From e05372f4d829d12da1f6351e8a2c55a22de69b47 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 12 Dec 2024 10:47:13 +0100 Subject: [PATCH 2/3] fix(SW-1168) Fixed typing --- .../SelectHotelMap/SelectHotelMapContent/index.tsx | 3 ++- .../SelectHotelMap/SelectHotelMapContent/utils.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx index ea9fc8212..f91165ea7 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -22,6 +22,7 @@ import { getVisibleHotels } from "./utils" import styles from "./selectHotelMapContent.module.css" +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map" export default function SelectHotelContent({ @@ -36,7 +37,7 @@ export default function SelectHotelContent({ const map = useMap() const isAboveMobile = useMediaQuery("(min-width: 768px)") - const [visibleHotels, setVisibleHotels] = useState([]) + const [visibleHotels, setVisibleHotels] = useState([]) const [showBackToTop, setShowBackToTop] = useState(false) const [isMapLoaded, setIsMapLoaded] = useState(false) const listingContainerRef = useRef(null) diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts index 481ad21ba..8dcba2169 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/utils.ts @@ -10,7 +10,7 @@ export function getVisibleHotelPins( const bounds = map.getBounds() if (!bounds) return [] - return filteredHotelPins.filter((pin: any) => { + return filteredHotelPins.filter((pin) => { const { lat, lng } = pin.coordinates return bounds.contains({ lat, lng }) }) @@ -22,8 +22,8 @@ export function getVisibleHotels( 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) + const visibleHotels = hotels.filter((hotel) => + visibleHotelPins.some((pin) => pin.operaId === hotel.hotelData.operaId) ) return visibleHotels } From e1bc7c25e067cac3efef55225883d11b7bdf25dd Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 12 Dec 2024 20:30:41 +0100 Subject: [PATCH 3/3] fix(SW-1168) Fixed comments for map --- .../SelectHotelMapContent/index.tsx | 25 +++++++++++++------ .../SelectHotel/SelectHotelMap/index.tsx | 4 +-- components/Maps/InteractiveMap/index.tsx | 4 +-- .../hotelPage/map/interactiveMap.ts | 4 +-- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx index f91165ea7..ed496c4cf 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -1,3 +1,4 @@ +"use client" import { useMap } from "@vis.gl/react-google-maps" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useIntl } from "react-intl" @@ -25,6 +26,8 @@ import styles from "./selectHotelMapContent.module.css" import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map" +const SKELETON_LOAD_DELAY = 750 + export default function SelectHotelContent({ hotelPins, cityCoordinates, @@ -39,7 +42,7 @@ export default function SelectHotelContent({ const isAboveMobile = useMediaQuery("(min-width: 768px)") const [visibleHotels, setVisibleHotels] = useState([]) const [showBackToTop, setShowBackToTop] = useState(false) - const [isMapLoaded, setIsMapLoaded] = useState(false) + const [showSkeleton, setShowSkeleton] = useState(false) const listingContainerRef = useRef(null) const activeFilters = useHotelFilterStore((state) => state.activeFilters) @@ -99,15 +102,21 @@ export default function SelectHotelContent({ const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map) setVisibleHotels(visibleHotels) setTimeout(() => { - setIsMapLoaded(true) - }, 750) + setShowSkeleton(true) + }, SKELETON_LOAD_DELAY) }, [hotels, filteredHotelPins, map]) + /** + * Updates visible hotels when map viewport changes (zoom/pan) + * - Debounces updates to prevent excessive re-renders during map interaction + * - Shows loading skeleton while map tiles load + * - Triggers on: initial load, zoom, pan, and tile loading completion + */ const debouncedUpdateHotelCards = useMemo( () => debounce(() => { if (!map) return - setIsMapLoaded(false) + setShowSkeleton(false) getHotelCards() }, 100), [map, getHotelCards] @@ -146,13 +155,13 @@ export default function SelectHotelContent({
- {isMapLoaded ? ( - - ) : ( + {showSkeleton ? ( <> + ) : ( + )} {showBackToTop && ( @@ -163,7 +172,7 @@ export default function SelectHotelContent({ coordinates={coordinates} hotelPins={filteredHotelPins} mapId={mapId} - onMapLoad={debouncedUpdateHotelCards} + onTilesLoaded={debouncedUpdateHotelCards} /> ) diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index a598050b1..a5b67d704 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -2,7 +2,7 @@ import { APIProvider } from "@vis.gl/react-google-maps" -import SelectHotelContent from "./SelectHotelMapContent" +import SelectHotelMapContent from "./SelectHotelMapContent" import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map" @@ -16,7 +16,7 @@ export default function SelectHotelMap({ }: SelectHotelMapProps) { return ( - - + {hotelPins && } {pointsOfInterest && ( void + onTilesLoaded?: () => void onActivePoiChange?: (poi: PointOfInterest["name"] | null) => void }