Merged in feat/SW-1750-map-connection (pull request #1439)

Feat(SW-1750): Destination page map connection 

Approved-by: Erik Tiekstra
This commit is contained in:
Matilda Landström
2025-03-03 07:56:40 +00:00
parent 21255f8557
commit 3f01266a75
8 changed files with 114 additions and 53 deletions

View File

@@ -6,6 +6,10 @@
overflow: hidden; overflow: hidden;
} }
.activeCard {
border: 1px solid var(--Border-Interactive-Selected);
}
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,8 +1,11 @@
"use client" "use client"
import Link from "next/link" import Link from "next/link"
import { useCallback, useEffect, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
import { TripAdvisorIcon } from "@/components/Icons" import { TripAdvisorIcon } from "@/components/Icons"
import HotelLogo from "@/components/Icons/Logos" import HotelLogo from "@/components/Icons/Logos"
@@ -28,8 +31,39 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || []) const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
const amenities = hotel.detailedFacilities.slice(0, 5) const amenities = hotel.detailedFacilities.slice(0, 5)
const itemRef = useRef<HTMLElement>(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 ( return (
<article className={styles.hotelListItem}> <article
ref={itemRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`${styles.hotelListItem} ${clickedHotel === hotel.operaId ? styles.activeCard : ""}`}
>
<div className={styles.imageWrapper}> <div className={styles.imageWrapper}>
<ImageGallery <ImageGallery
images={galleryImages} images={galleryImages}

View File

@@ -13,12 +13,11 @@
transition: all 0.3s; transition: all 0.3s;
} }
.clusterMarker:hover { .clusterMarker:hover,
background: linear-gradient( .hoveredChild {
rgba(255, 255, 255, 0.2), background:
rgba(255, 255, 255, 0.2) linear-gradient(rgba(31, 28, 27, 0.3), rgba(31, 28, 27, 0.3)),
), var(--Surface-Brand-Primary-2-Default);
var(--Base-Text-High-contrast);
} }
.count { .count {

View File

@@ -6,6 +6,8 @@ import {
} from "@vis.gl/react-google-maps" } from "@vis.gl/react-google-maps"
import { useCallback } from "react" import { useCallback } from "react"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import styles from "./clusterMarker.module.css" import styles from "./clusterMarker.module.css"
interface ClusterMarkerProps { interface ClusterMarkerProps {
@@ -13,6 +15,7 @@ interface ClusterMarkerProps {
size: number size: number
sizeAsText: string sizeAsText: string
onMarkerClick?: (position: google.maps.LatLngLiteral) => void onMarkerClick?: (position: google.maps.LatLngLiteral) => void
hotelIds: number[]
} }
export default function ClusterMarker({ export default function ClusterMarker({
@@ -20,7 +23,13 @@ export default function ClusterMarker({
size, size,
sizeAsText, sizeAsText,
onMarkerClick, onMarkerClick,
hotelIds,
}: ClusterMarkerProps) { }: ClusterMarkerProps) {
const { hoveredHotel } = useDestinationPageHotelsMapStore()
const isActive = hoveredHotel
? hotelIds.includes(Number(hoveredHotel))
: false
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (onMarkerClick) { if (onMarkerClick) {
onMarkerClick(position) onMarkerClick(position)
@@ -32,7 +41,7 @@ export default function ClusterMarker({
position={position} position={position}
zIndex={size} zIndex={size}
onClick={handleClick} onClick={handleClick}
className={styles.clusterMarker} className={`${styles.clusterMarker} ${isActive ? styles.hoveredChild : ""}`}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER} anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
> >
<span className={styles.count}>{sizeAsText}</span> <span className={styles.count}>{sizeAsText}</span>

View File

@@ -5,7 +5,9 @@ import {
AdvancedMarkerAnchorPoint, AdvancedMarkerAnchorPoint,
useAdvancedMarkerRef, useAdvancedMarkerRef,
} from "@vis.gl/react-google-maps" } 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" import HotelMarkerByType from "@/components/Maps/Markers"
@@ -16,42 +18,42 @@ import type { MarkerProperties } from "@/types/components/maps/destinationMarker
interface MarkerProps { interface MarkerProps {
position: google.maps.LatLngLiteral position: google.maps.LatLngLiteral
properties: MarkerProperties properties: MarkerProperties
isActive: boolean
onMarkerClick?: (properties: MarkerProperties) => void
onCloseMapCard?: () => void
} }
export default function Marker({ export default function Marker({ position, properties }: MarkerProps) {
position,
properties,
isActive,
onMarkerClick,
onCloseMapCard,
}: MarkerProps) {
const [markerRef] = useAdvancedMarkerRef() const [markerRef] = useAdvancedMarkerRef()
const [isHovered, setIsHovered] = useState(false)
const { setHoveredHotel, setClickedHotel, hoveredHotel, clickedHotel } =
useDestinationPageHotelsMapStore()
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (onMarkerClick) { setClickedHotel(properties.id)
onMarkerClick(properties) }, [setClickedHotel, properties])
}
}, [onMarkerClick, properties])
function handleCloseCard() { function handleCloseCard() {
if (onCloseMapCard) { setClickedHotel(null)
onCloseMapCard()
}
} }
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 ( return (
<AdvancedMarker <AdvancedMarker
ref={markerRef} ref={markerRef}
position={position} position={position}
onClick={handleClick} onClick={handleClick}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={handleMouseEnter}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={handleMouseLeave}
anchorPoint={AdvancedMarkerAnchorPoint.BOTTOM_CENTER} anchorPoint={AdvancedMarkerAnchorPoint.BOTTOM_CENTER}
zIndex={isActive ? 10 : 0} zIndex={isActive ? 300 : 0}
> >
<HotelMarkerByType <HotelMarkerByType
smallSize={!isHovered && !isActive} smallSize={!isHovered && !isActive}

View File

@@ -1,7 +1,9 @@
"use client" "use client"
import { useMap } from "@vis.gl/react-google-maps" import { useMap } from "@vis.gl/react-google-maps"
import { useEffect, useState } from "react" import { useEffect } from "react"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import { useSupercluster } from "@/hooks/maps/use-supercluster" import { useSupercluster } from "@/hooks/maps/use-supercluster"
@@ -28,23 +30,20 @@ const CLUSTER_OPTIONS = {
} }
export default function MapContent({ geojson }: MapContentProps) { export default function MapContent({ geojson }: MapContentProps) {
const [activeMarker, setActiveMarker] = useState<string | undefined>( const { setClickedHotel, clickedHotel } = useDestinationPageHotelsMapStore()
undefined
)
const map = useMap() const map = useMap()
const { clusters } = useSupercluster<MarkerProperties>( const { clusters, containedHotels } = useSupercluster<MarkerProperties>(
geojson, geojson,
CLUSTER_OPTIONS CLUSTER_OPTIONS
) )
useEffect(() => { useEffect(() => {
map?.addListener("click", () => { map?.addListener("click", () => {
if (activeMarker) { if (clickedHotel) {
setActiveMarker(undefined) setClickedHotel(null)
} }
}) })
}, [activeMarker, map]) }, [clickedHotel, map, setClickedHotel])
function handleClusterClick(position: google.maps.LatLngLiteral) { function handleClusterClick(position: google.maps.LatLngLiteral) {
const currentZoom = map && map.getZoom() const currentZoom = map && map.getZoom()
@@ -54,17 +53,8 @@ export default function MapContent({ geojson }: MapContentProps) {
} }
} }
function handleMarkerClick(properties: MarkerProperties) { return clusters.map((feature, idx) => {
setActiveMarker(properties?.id)
}
function handleCloseMapCard() {
setActiveMarker(undefined)
}
return clusters.map((feature) => {
const [lng, lat] = feature.geometry.coordinates const [lng, lat] = feature.geometry.coordinates
const clusterProperties = feature.properties as ClusterProperties const clusterProperties = feature.properties as ClusterProperties
const markerProperties = feature.properties as MarkerProperties const markerProperties = feature.properties as MarkerProperties
const isCluster = clusterProperties.cluster const isCluster = clusterProperties.cluster
@@ -76,15 +66,13 @@ export default function MapContent({ geojson }: MapContentProps) {
size={clusterProperties.point_count} size={clusterProperties.point_count}
sizeAsText={String(clusterProperties.point_count_abbreviated)} sizeAsText={String(clusterProperties.point_count_abbreviated)}
onMarkerClick={handleClusterClick} onMarkerClick={handleClusterClick}
hotelIds={containedHotels[idx]}
/> />
) : ( ) : (
<Marker <Marker
key={feature.id} key={feature.id}
position={{ lat, lng }} position={{ lat, lng }}
properties={markerProperties} properties={markerProperties}
onMarkerClick={handleMarkerClick}
onCloseMapCard={handleCloseMapCard}
isActive={activeMarker === feature.id}
/> />
) )
}) })

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useReducer } from "react" import { useEffect, useMemo, useReducer } from "react"
import Supercluster, {type ClusterProperties } from "supercluster" import Supercluster, { type ClusterProperties } from "supercluster"
import { useMapViewport } from "./use-map-viewport" import { useMapViewport } from "./use-map-viewport"
@@ -36,7 +36,16 @@ export function useSupercluster<T extends GeoJsonProperties>(
return clusterer.getClusters(bbox, zoom) return clusterer.getClusters(bbox, zoom)
}, [version, clusterer, 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 { return {
clusters, clusters,
containedHotels,
} }
} }

View File

@@ -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<DestinationPageHotelsMapState>((set) => ({
hoveredHotel: null,
clickedHotel: null,
setHoveredHotel: (hotelId) => set({ hoveredHotel: hotelId }),
setClickedHotel: (hotelId) => set({ clickedHotel: hotelId }),
}))