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