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;
}
.activeCard {
border: 1px solid var(--Border-Interactive-Selected);
}
.content {
display: flex;
flex-direction: column;

View File

@@ -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<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 (
<article className={styles.hotelListItem}>
<article
ref={itemRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`${styles.hotelListItem} ${clickedHotel === hotel.operaId ? styles.activeCard : ""}`}
>
<div className={styles.imageWrapper}>
<ImageGallery
images={galleryImages}

View File

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

View File

@@ -6,6 +6,8 @@ import {
} from "@vis.gl/react-google-maps"
import { useCallback } from "react"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import styles from "./clusterMarker.module.css"
interface ClusterMarkerProps {
@@ -13,6 +15,7 @@ interface ClusterMarkerProps {
size: number
sizeAsText: string
onMarkerClick?: (position: google.maps.LatLngLiteral) => 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}
>
<span className={styles.count}>{sizeAsText}</span>

View File

@@ -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 (
<AdvancedMarker
ref={markerRef}
position={position}
onClick={handleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
anchorPoint={AdvancedMarkerAnchorPoint.BOTTOM_CENTER}
zIndex={isActive ? 10 : 0}
zIndex={isActive ? 300 : 0}
>
<HotelMarkerByType
smallSize={!isHovered && !isActive}

View File

@@ -1,7 +1,9 @@
"use client"
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"
@@ -28,23 +30,20 @@ const CLUSTER_OPTIONS = {
}
export default function MapContent({ geojson }: MapContentProps) {
const [activeMarker, setActiveMarker] = useState<string | undefined>(
undefined
)
const { setClickedHotel, clickedHotel } = useDestinationPageHotelsMapStore()
const map = useMap()
const { clusters } = useSupercluster<MarkerProperties>(
const { clusters, containedHotels } = useSupercluster<MarkerProperties>(
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]}
/>
) : (
<Marker
key={feature.id}
position={{ lat, lng }}
properties={markerProperties}
onMarkerClick={handleMarkerClick}
onCloseMapCard={handleCloseMapCard}
isActive={activeMarker === feature.id}
/>
)
})

View File

@@ -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<T extends GeoJsonProperties>(
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,
}
}

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 }),
}))