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:
@@ -6,6 +6,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.activeCard {
|
||||
border: 1px solid var(--Border-Interactive-Selected);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
16
apps/scandic-web/stores/destination-page-hotels-map.ts
Normal file
16
apps/scandic-web/stores/destination-page-hotels-map.ts
Normal 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 }),
|
||||
}))
|
||||
Reference in New Issue
Block a user