From 3f01266a75cee391356586afeca4116a49544b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= Date: Mon, 3 Mar 2025 07:56:40 +0000 Subject: [PATCH] Merged in feat/SW-1750-map-connection (pull request #1439) Feat(SW-1750): Destination page map connection Approved-by: Erik Tiekstra --- .../HotelListItem/hotelListItem.module.css | 4 ++ .../CityMap/HotelListItem/index.tsx | 36 ++++++++++++++- .../ClusterMarker/clusterMarker.module.css | 11 ++--- .../Map/MapContent/ClusterMarker/index.tsx | 11 ++++- .../Map/MapContent/Marker/index.tsx | 46 ++++++++++--------- .../DestinationPage/Map/MapContent/index.tsx | 32 ++++--------- .../hooks/maps/use-supercluster.ts | 11 ++++- .../stores/destination-page-hotels-map.ts | 16 +++++++ 8 files changed, 114 insertions(+), 53 deletions(-) create mode 100644 apps/scandic-web/stores/destination-page-hotels-map.ts diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css index 2ab13683b..4ae21f4c1 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/hotelListItem.module.css @@ -6,6 +6,10 @@ overflow: hidden; } +.activeCard { + border: 1px solid var(--Border-Interactive-Selected); +} + .content { display: flex; flex-direction: column; diff --git a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx index 7b1690a3e..fa5a12f47 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/DestinationCityPage/CityMap/HotelListItem/index.tsx @@ -1,8 +1,11 @@ "use client" import Link from "next/link" +import { useCallback, useEffect, useRef } from "react" import { useIntl } from "react-intl" +import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { TripAdvisorIcon } from "@/components/Icons" import HotelLogo from "@/components/Icons/Logos" @@ -28,8 +31,39 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) { const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || []) const amenities = hotel.detailedFacilities.slice(0, 5) + const itemRef = useRef(null) + const { setHoveredHotel, clickedHotel } = useDestinationPageHotelsMapStore() + + useEffect(() => { + if (clickedHotel === hotel.operaId) { + const element = itemRef.current + if (element) { + element.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "start", + }) + } + } + }, [clickedHotel, hotel.operaId]) + + const handleMouseEnter = useCallback(() => { + if (hotel.operaId) { + setHoveredHotel(hotel.operaId) + } + }, [setHoveredHotel, hotel.operaId]) + + const handleMouseLeave = useCallback(() => { + setHoveredHotel(null) + }, [setHoveredHotel]) + return ( -
+
void + hotelIds: number[] } export default function ClusterMarker({ @@ -20,7 +23,13 @@ export default function ClusterMarker({ size, sizeAsText, onMarkerClick, + hotelIds, }: ClusterMarkerProps) { + const { hoveredHotel } = useDestinationPageHotelsMapStore() + const isActive = hoveredHotel + ? hotelIds.includes(Number(hoveredHotel)) + : false + const handleClick = useCallback(() => { if (onMarkerClick) { onMarkerClick(position) @@ -32,7 +41,7 @@ export default function ClusterMarker({ position={position} zIndex={size} onClick={handleClick} - className={styles.clusterMarker} + className={`${styles.clusterMarker} ${isActive ? styles.hoveredChild : ""}`} anchorPoint={AdvancedMarkerAnchorPoint.CENTER} > {sizeAsText} diff --git a/apps/scandic-web/components/ContentType/DestinationPage/Map/MapContent/Marker/index.tsx b/apps/scandic-web/components/ContentType/DestinationPage/Map/MapContent/Marker/index.tsx index ee24710e7..0d854c924 100644 --- a/apps/scandic-web/components/ContentType/DestinationPage/Map/MapContent/Marker/index.tsx +++ b/apps/scandic-web/components/ContentType/DestinationPage/Map/MapContent/Marker/index.tsx @@ -5,7 +5,9 @@ import { AdvancedMarkerAnchorPoint, useAdvancedMarkerRef, } from "@vis.gl/react-google-maps" -import { useCallback, useState } from "react" +import { useCallback } from "react" + +import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map" import HotelMarkerByType from "@/components/Maps/Markers" @@ -16,42 +18,42 @@ import type { MarkerProperties } from "@/types/components/maps/destinationMarker interface MarkerProps { position: google.maps.LatLngLiteral properties: MarkerProperties - isActive: boolean - onMarkerClick?: (properties: MarkerProperties) => void - onCloseMapCard?: () => void } -export default function Marker({ - position, - properties, - isActive, - onMarkerClick, - onCloseMapCard, -}: MarkerProps) { +export default function Marker({ position, properties }: MarkerProps) { const [markerRef] = useAdvancedMarkerRef() - const [isHovered, setIsHovered] = useState(false) + + const { setHoveredHotel, setClickedHotel, hoveredHotel, clickedHotel } = + useDestinationPageHotelsMapStore() const handleClick = useCallback(() => { - if (onMarkerClick) { - onMarkerClick(properties) - } - }, [onMarkerClick, properties]) + setClickedHotel(properties.id) + }, [setClickedHotel, properties]) function handleCloseCard() { - if (onCloseMapCard) { - onCloseMapCard() - } + setClickedHotel(null) } + const handleMouseEnter = useCallback(() => { + setHoveredHotel(properties.id) + }, [setHoveredHotel, properties.id]) + + const handleMouseLeave = useCallback(() => { + setHoveredHotel(null) + }, [setHoveredHotel]) + + const isHovered = hoveredHotel === properties.id + const isActive = clickedHotel === properties.id + return ( setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} anchorPoint={AdvancedMarkerAnchorPoint.BOTTOM_CENTER} - zIndex={isActive ? 10 : 0} + zIndex={isActive ? 300 : 0} > ( - undefined - ) - + const { setClickedHotel, clickedHotel } = useDestinationPageHotelsMapStore() const map = useMap() - const { clusters } = useSupercluster( + const { clusters, containedHotels } = useSupercluster( geojson, CLUSTER_OPTIONS ) useEffect(() => { map?.addListener("click", () => { - if (activeMarker) { - setActiveMarker(undefined) + if (clickedHotel) { + setClickedHotel(null) } }) - }, [activeMarker, map]) + }, [clickedHotel, map, setClickedHotel]) function handleClusterClick(position: google.maps.LatLngLiteral) { const currentZoom = map && map.getZoom() @@ -54,17 +53,8 @@ export default function MapContent({ geojson }: MapContentProps) { } } - function handleMarkerClick(properties: MarkerProperties) { - setActiveMarker(properties?.id) - } - - function handleCloseMapCard() { - setActiveMarker(undefined) - } - - return clusters.map((feature) => { + return clusters.map((feature, idx) => { const [lng, lat] = feature.geometry.coordinates - const clusterProperties = feature.properties as ClusterProperties const markerProperties = feature.properties as MarkerProperties const isCluster = clusterProperties.cluster @@ -76,15 +66,13 @@ export default function MapContent({ geojson }: MapContentProps) { size={clusterProperties.point_count} sizeAsText={String(clusterProperties.point_count_abbreviated)} onMarkerClick={handleClusterClick} + hotelIds={containedHotels[idx]} /> ) : ( ) }) diff --git a/apps/scandic-web/hooks/maps/use-supercluster.ts b/apps/scandic-web/hooks/maps/use-supercluster.ts index 249bbad00..bb8a958fa 100644 --- a/apps/scandic-web/hooks/maps/use-supercluster.ts +++ b/apps/scandic-web/hooks/maps/use-supercluster.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo, useReducer } from "react" -import Supercluster, {type ClusterProperties } from "supercluster" +import Supercluster, { type ClusterProperties } from "supercluster" import { useMapViewport } from "./use-map-viewport" @@ -36,7 +36,16 @@ export function useSupercluster( return clusterer.getClusters(bbox, zoom) }, [version, clusterer, bbox, zoom]) + // retrieve the hotel ids included in the cluster + const containedHotels = clusters.map((cluster) => { + if (cluster.properties?.cluster && typeof cluster.id === "number") { + return clusterer.getLeaves(cluster.id).map((hotel) => Number(hotel.id)) + } + return [] + }) + return { clusters, + containedHotels, } } diff --git a/apps/scandic-web/stores/destination-page-hotels-map.ts b/apps/scandic-web/stores/destination-page-hotels-map.ts new file mode 100644 index 000000000..d6aecc27d --- /dev/null +++ b/apps/scandic-web/stores/destination-page-hotels-map.ts @@ -0,0 +1,16 @@ +import { create } from "zustand" + +interface DestinationPageHotelsMapState { + hoveredHotel: string | null + clickedHotel: string | null + setHoveredHotel: (hotelId: string | null) => void + setClickedHotel: (hotelId: string | null) => void +} + +export const useDestinationPageHotelsMapStore = + create((set) => ({ + hoveredHotel: null, + clickedHotel: null, + setHoveredHotel: (hotelId) => set({ hoveredHotel: hotelId }), + setClickedHotel: (hotelId) => set({ clickedHotel: hotelId }), + }))