Feat/SW-1937 destination overview map mobile
* feat(SW-1937): Added gestureHandling prop to be able to scroll past the map on the overview page * feat(SW-1937): Added active map card on overview page for smaller viewports Approved-by: Matilda Landström
This commit is contained in:
@@ -32,10 +32,10 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
|||||||
const amenities = hotel.detailedFacilities.slice(0, 5)
|
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||||
|
|
||||||
const itemRef = useRef<HTMLElement>(null)
|
const itemRef = useRef<HTMLElement>(null)
|
||||||
const { setHoveredHotel, activeHotel } = useDestinationPageHotelsMapStore()
|
const { setHoveredMarker, activeMarker } = useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeHotel === hotel.operaId) {
|
if (activeMarker === hotel.operaId) {
|
||||||
const element = itemRef.current
|
const element = itemRef.current
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
@@ -45,24 +45,24 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [activeHotel, hotel.operaId])
|
}, [activeMarker, hotel.operaId])
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
if (hotel.operaId) {
|
if (hotel.operaId) {
|
||||||
setHoveredHotel(hotel.operaId)
|
setHoveredMarker(hotel.operaId)
|
||||||
}
|
}
|
||||||
}, [setHoveredHotel, hotel.operaId])
|
}, [setHoveredMarker, hotel.operaId])
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
setHoveredHotel(null)
|
setHoveredMarker(null)
|
||||||
}, [setHoveredHotel])
|
}, [setHoveredMarker])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
className={`${styles.hotelListItem} ${activeHotel === hotel.operaId ? styles.activeCard : ""}`}
|
className={`${styles.hotelListItem} ${activeMarker === hotel.operaId ? styles.activeCard : ""}`}
|
||||||
>
|
>
|
||||||
<div className={styles.imageWrapper}>
|
<div className={styles.imageWrapper}>
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.activeMapCard {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--Space-x2);
|
||||||
|
left: var(--Space-x2);
|
||||||
|
right: var(--Space-x2);
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
|
import HotelMapCard from "../../../HotelMapCard"
|
||||||
|
|
||||||
|
import styles from "./activeMapCard.module.css"
|
||||||
|
|
||||||
|
import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
|
||||||
|
|
||||||
|
interface ActiveMapCardProps {
|
||||||
|
markers: DestinationMarker[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ActiveMapCard({ markers }: ActiveMapCardProps) {
|
||||||
|
const { activeMarker } = useDestinationPageHotelsMapStore()
|
||||||
|
const activeMapCardVisible = useMediaQuery("(max-width: 949px)")
|
||||||
|
|
||||||
|
if (!activeMarker || !activeMapCardVisible) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeHotel = markers.find((marker) => marker.id === activeMarker)
|
||||||
|
|
||||||
|
if (!activeHotel) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotelMapCard
|
||||||
|
className={styles.activeMapCard}
|
||||||
|
amenities={activeHotel.amenities}
|
||||||
|
tripadvisorRating={activeHotel.tripadvisor}
|
||||||
|
hotelName={activeHotel.name}
|
||||||
|
image={activeHotel.image}
|
||||||
|
url={activeHotel.url}
|
||||||
|
type="article"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import DynamicMap from "../../Map/DynamicMap"
|
|||||||
import MapContent from "../../Map/MapContent"
|
import MapContent from "../../Map/MapContent"
|
||||||
import MapProvider from "../../Map/MapProvider"
|
import MapProvider from "../../Map/MapProvider"
|
||||||
import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "../../Map/utils"
|
import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "../../Map/utils"
|
||||||
|
import ActiveMapCard from "./ActiveMapCard"
|
||||||
import InputForm from "./InputForm"
|
import InputForm from "./InputForm"
|
||||||
|
|
||||||
import type { MapLocation } from "@/types/components/mapLocation"
|
import type { MapLocation } from "@/types/components/mapLocation"
|
||||||
@@ -43,8 +44,10 @@ export default async function OverviewMapContainer({
|
|||||||
markers={markers}
|
markers={markers}
|
||||||
defaultCoordinates={defaultCoordinates}
|
defaultCoordinates={defaultCoordinates}
|
||||||
defaultZoom={defaultZoom}
|
defaultZoom={defaultZoom}
|
||||||
|
gestureHandling="cooperative"
|
||||||
>
|
>
|
||||||
<MapContent geojson={geoJson} />
|
<MapContent geojson={geoJson} />
|
||||||
|
<ActiveMapCard markers={markers} />
|
||||||
</DynamicMap>
|
</DynamicMap>
|
||||||
</MapProvider>
|
</MapProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,19 +19,19 @@ interface MapCardCarouselProps {
|
|||||||
export default function HotelCardCarousel({
|
export default function HotelCardCarousel({
|
||||||
visibleHotels,
|
visibleHotels,
|
||||||
}: MapCardCarouselProps) {
|
}: MapCardCarouselProps) {
|
||||||
const { activeHotel, setActiveHotel } = useDestinationPageHotelsMapStore()
|
const { activeMarker, setActiveMarker } = useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
const selectedHotelIdx = visibleHotels.findIndex(
|
const selectedHotelIdx = visibleHotels.findIndex(
|
||||||
({ hotel }) => hotel.operaId === activeHotel
|
({ hotel }) => hotel.operaId === activeMarker
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleScrollSelect = useCallback(
|
const handleScrollSelect = useCallback(
|
||||||
(idx: number) => {
|
(idx: number) => {
|
||||||
if (selectedHotelIdx !== -1) {
|
if (selectedHotelIdx !== -1) {
|
||||||
setActiveHotel(visibleHotels[idx]?.hotel.operaId)
|
setActiveMarker(visibleHotels[idx]?.hotel.operaId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setActiveHotel, visibleHotels, selectedHotelIdx]
|
[setActiveMarker, visibleHotels, selectedHotelIdx]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,7 +47,7 @@ export default function HotelCardCarousel({
|
|||||||
<Carousel.Item key={hotel.operaId} className={styles.item}>
|
<Carousel.Item key={hotel.operaId} className={styles.item}>
|
||||||
<HotelMapCard
|
<HotelMapCard
|
||||||
className={cx(styles.carouselCard, {
|
className={cx(styles.carouselCard, {
|
||||||
[styles.noActiveHotel]: !activeHotel,
|
[styles.noActiveHotel]: !activeMarker,
|
||||||
})}
|
})}
|
||||||
tripadvisorRating={hotel.ratings?.tripAdvisor.rating}
|
tripadvisorRating={hotel.ratings?.tripAdvisor.rating}
|
||||||
hotelName={hotel.name}
|
hotelName={hotel.name}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function HotelListingItem({
|
|||||||
}: HotelListingItemProps) {
|
}: HotelListingItemProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const { setActiveHotel } = useDestinationPageHotelsMapStore()
|
const { setActiveMarker } = useDestinationPageHotelsMapStore()
|
||||||
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 [mapUrl, setMapUrl] = useState<string | null>(null)
|
const [mapUrl, setMapUrl] = useState<string | null>(null)
|
||||||
@@ -108,7 +108,7 @@ export default function HotelListingItem({
|
|||||||
<Link
|
<Link
|
||||||
href={mapUrl}
|
href={mapUrl}
|
||||||
scroll={true}
|
scroll={true}
|
||||||
onClick={() => setActiveHotel(hotel.operaId)}
|
onClick={() => setActiveMarker(hotel.operaId)}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "See on map" })}
|
{intl.formatMessage({ id: "See on map" })}
|
||||||
<ChevronRightSmallIcon />
|
<ChevronRightSmallIcon />
|
||||||
|
|||||||
@@ -9,8 +9,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.listHeader {
|
.listHeader {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotelList {
|
.hotelList {
|
||||||
@@ -26,3 +32,14 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.listHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapButton {
|
||||||
|
display: none !important; /* Important to override button higher specificy */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRef } from "react"
|
import Link from "next/link"
|
||||||
|
import { useParams } from "next/navigation"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||||
|
|
||||||
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
||||||
|
import { MapIcon } from "@/components/Icons"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||||
|
|
||||||
@@ -21,6 +25,8 @@ import { AlertTypeEnum } from "@/types/enums/alert"
|
|||||||
export default function HotelListing() {
|
export default function HotelListing() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const scrollRef = useRef<HTMLElement>(null)
|
const scrollRef = useRef<HTMLElement>(null)
|
||||||
|
const params = useParams()
|
||||||
|
const [mapUrl, setMapUrl] = useState<string | null>(null)
|
||||||
const { showBackToTop, scrollToTop } = useScrollToTop({
|
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||||
threshold: 300,
|
threshold: 300,
|
||||||
elementRef: scrollRef,
|
elementRef: scrollRef,
|
||||||
@@ -30,6 +36,12 @@ export default function HotelListing() {
|
|||||||
isLoading: state.isLoading,
|
isLoading: state.isLoading,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.searchParams.set("view", "map")
|
||||||
|
setMapUrl(url.toString())
|
||||||
|
}, [params])
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<HotelListingSkeleton />
|
<HotelListingSkeleton />
|
||||||
) : (
|
) : (
|
||||||
@@ -43,7 +55,24 @@ export default function HotelListing() {
|
|||||||
{ count: activeHotels.length }
|
{ count: activeHotels.length }
|
||||||
)}
|
)}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<DestinationFilterAndSort listType="hotel" />
|
<div className={styles.cta}>
|
||||||
|
{mapUrl && (
|
||||||
|
<Button
|
||||||
|
className={styles.mapButton}
|
||||||
|
asChild
|
||||||
|
intent="secondary"
|
||||||
|
variant="icon"
|
||||||
|
size="small"
|
||||||
|
theme="base"
|
||||||
|
>
|
||||||
|
<Link href={mapUrl}>
|
||||||
|
<MapIcon />
|
||||||
|
{intl.formatMessage({ id: "See on map" })}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<DestinationFilterAndSort listType="hotel" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{activeHotels.length === 0 ? (
|
{activeHotels.length === 0 ? (
|
||||||
<Alert
|
<Alert
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ export default function HotelMapCard({
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const pageType = usePageType()
|
const pageType = usePageType()
|
||||||
const [imageError, setImageError] = useState(false)
|
const [imageError, setImageError] = useState(false)
|
||||||
const { setActiveHotel } = useDestinationPageHotelsMapStore()
|
const { setActiveMarker } = useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
setActiveHotel(null)
|
setActiveMarker(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Wrapper({ children }: React.PropsWithChildren) {
|
function Wrapper({ children }: React.PropsWithChildren) {
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
import "client-only"
|
import "client-only"
|
||||||
|
|
||||||
import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
|
import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
|
||||||
import { type PropsWithChildren, useEffect } from "react"
|
import { type PropsWithChildren, useEffect, useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
import ErrorBoundary from "@/components/ErrorBoundary/ErrorBoundary"
|
import ErrorBoundary from "@/components/ErrorBoundary/ErrorBoundary"
|
||||||
import { CloseLargeIcon, MinusIcon, PlusIcon } from "@/components/Icons"
|
import { CloseLargeIcon, MinusIcon, PlusIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||||
|
|
||||||
|
import { usePageType } from "../PageTypeProvider"
|
||||||
|
|
||||||
import styles from "./dynamicMap.module.css"
|
import styles from "./dynamicMap.module.css"
|
||||||
|
|
||||||
import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
|
import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
|
||||||
@@ -26,6 +30,7 @@ interface DynamicMapProps {
|
|||||||
defaultCoordinates: google.maps.LatLngLiteral | null
|
defaultCoordinates: google.maps.LatLngLiteral | null
|
||||||
defaultZoom: number
|
defaultZoom: number
|
||||||
fitBounds?: boolean
|
fitBounds?: boolean
|
||||||
|
gestureHandling?: "greedy" | "cooperative" | "auto" | "none"
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +41,23 @@ export default function DynamicMap({
|
|||||||
defaultZoom,
|
defaultZoom,
|
||||||
fitBounds = true,
|
fitBounds = true,
|
||||||
onClose,
|
onClose,
|
||||||
|
gestureHandling = "auto",
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<DynamicMapProps>) {
|
}: PropsWithChildren<DynamicMapProps>) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
|
const pageType = usePageType()
|
||||||
|
const { activeMarker } = useDestinationPageHotelsMapStore()
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current && activeMarker && pageType === "overview") {
|
||||||
|
ref.current.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "end",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [activeMarker, pageType])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map && fitBounds) {
|
if (map && fitBounds) {
|
||||||
@@ -81,12 +99,12 @@ export default function DynamicMap({
|
|||||||
defaultZoom,
|
defaultZoom,
|
||||||
disableDefaultUI: true,
|
disableDefaultUI: true,
|
||||||
clickableIcons: false,
|
clickableIcons: false,
|
||||||
gestureHandling: "greedy",
|
gestureHandling,
|
||||||
mapId,
|
mapId,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mapWrapper}>
|
<div className={styles.mapWrapper} ref={ref}>
|
||||||
<ErrorBoundary fallback={<h2>Unable to display map</h2>}>
|
<ErrorBoundary fallback={<h2>Unable to display map</h2>}>
|
||||||
<Map {...mapOptions}>{children}</Map>
|
<Map {...mapOptions}>{children}</Map>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ export default function ClusterMarker({
|
|||||||
onMarkerClick,
|
onMarkerClick,
|
||||||
hotelIds,
|
hotelIds,
|
||||||
}: ClusterMarkerProps) {
|
}: ClusterMarkerProps) {
|
||||||
const { hoveredHotel, activeHotel } = useDestinationPageHotelsMapStore()
|
const { hoveredMarker, activeMarker } = useDestinationPageHotelsMapStore()
|
||||||
const isActive =
|
const isActive =
|
||||||
hotelIds.includes(Number(hoveredHotel)) ||
|
hotelIds.includes(Number(hoveredMarker)) ||
|
||||||
hotelIds.includes(Number(activeHotel))
|
hotelIds.includes(Number(activeMarker))
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (onMarkerClick) {
|
if (onMarkerClick) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
AdvancedMarkerAnchorPoint,
|
AdvancedMarkerAnchorPoint,
|
||||||
useAdvancedMarkerRef,
|
useAdvancedMarkerRef,
|
||||||
} from "@vis.gl/react-google-maps"
|
} from "@vis.gl/react-google-maps"
|
||||||
import { useCallback } from "react"
|
|
||||||
|
|
||||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
@@ -24,30 +23,30 @@ interface MarkerProps {
|
|||||||
export default function Marker({ position, properties }: MarkerProps) {
|
export default function Marker({ position, properties }: MarkerProps) {
|
||||||
const [markerRef] = useAdvancedMarkerRef()
|
const [markerRef] = useAdvancedMarkerRef()
|
||||||
|
|
||||||
const { setHoveredHotel, setActiveHotel, hoveredHotel, activeHotel } =
|
const { setHoveredMarker, setActiveMarker, hoveredMarker, activeMarker } =
|
||||||
useDestinationPageHotelsMapStore()
|
useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
function handleMarkerClick() {
|
||||||
setActiveHotel(properties.id)
|
setActiveMarker(properties.id)
|
||||||
trackMapClick(properties.name)
|
trackMapClick(properties.name)
|
||||||
}, [setActiveHotel, properties])
|
}
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
function handleMouseEnter() {
|
||||||
setHoveredHotel(properties.id)
|
setHoveredMarker(properties.id)
|
||||||
}, [setHoveredHotel, properties.id])
|
}
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
function handleMouseLeave() {
|
||||||
setHoveredHotel(null)
|
setHoveredMarker(null)
|
||||||
}, [setHoveredHotel])
|
}
|
||||||
|
|
||||||
const isHovered = hoveredHotel === properties.id
|
const isHovered = hoveredMarker === properties.id
|
||||||
const isActive = activeHotel === properties.id
|
const isActive = activeMarker === properties.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdvancedMarker
|
<AdvancedMarker
|
||||||
ref={markerRef}
|
ref={markerRef}
|
||||||
position={position}
|
position={position}
|
||||||
onClick={handleClick}
|
onClick={handleMarkerClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
anchorPoint={AdvancedMarkerAnchorPoint.BOTTOM_CENTER}
|
anchorPoint={AdvancedMarkerAnchorPoint.BOTTOM_CENTER}
|
||||||
|
|||||||
@@ -34,30 +34,33 @@ export default function MapContent({
|
|||||||
geojson,
|
geojson,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
}: MapContentProps) {
|
}: MapContentProps) {
|
||||||
const { setActiveHotel, activeHotel } = useDestinationPageHotelsMapStore()
|
const { setActiveMarker, activeMarker } = useDestinationPageHotelsMapStore()
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
|
|
||||||
const { clusters, containedHotels } = useSupercluster<MarkerProperties>(
|
const { clusters, containedHotels, getClusterZoom } =
|
||||||
geojson,
|
useSupercluster<MarkerProperties>(geojson, CLUSTER_OPTIONS)
|
||||||
CLUSTER_OPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
// Based on the length of active filters, we decide if should show clusters or individual markers
|
// Based on the length of active filters, we decide if should show clusters or individual markers
|
||||||
const markerList = hasActiveFilters ? geojson.features : clusters
|
const markerList = hasActiveFilters ? geojson.features : clusters
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map?.addListener("click", () => {
|
map?.addListener("click", () => {
|
||||||
if (activeHotel) {
|
if (activeMarker) {
|
||||||
setActiveHotel(null)
|
setActiveMarker(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [activeHotel, map, setActiveHotel])
|
}, [activeMarker, map, setActiveMarker])
|
||||||
|
|
||||||
function handleClusterClick(position: google.maps.LatLngLiteral) {
|
function handleClusterClick(
|
||||||
|
position: google.maps.LatLngLiteral,
|
||||||
|
clusterProperties: ClusterProperties
|
||||||
|
) {
|
||||||
const currentZoom = map && map.getZoom()
|
const currentZoom = map && map.getZoom()
|
||||||
|
const clusterZoom = getClusterZoom(clusterProperties.cluster_id)
|
||||||
|
|
||||||
if (currentZoom) {
|
if (currentZoom) {
|
||||||
map.panTo(position)
|
map.panTo(position)
|
||||||
map.setZoom(currentZoom + 2)
|
map.setZoom(clusterZoom ?? currentZoom + 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +76,9 @@ export default function MapContent({
|
|||||||
position={{ lat, lng }}
|
position={{ lat, lng }}
|
||||||
size={clusterProperties.point_count}
|
size={clusterProperties.point_count}
|
||||||
sizeAsText={String(clusterProperties.point_count_abbreviated)}
|
sizeAsText={String(clusterProperties.point_count_abbreviated)}
|
||||||
onMarkerClick={handleClusterClick}
|
onMarkerClick={(position) =>
|
||||||
|
handleClusterClick(position, clusterProperties)
|
||||||
|
}
|
||||||
hotelIds={containedHotels[idx]}
|
hotelIds={containedHotels[idx]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function Map({
|
|||||||
}: PropsWithChildren<MapProps>) {
|
}: PropsWithChildren<MapProps>) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { activeHotel: activeHotelId } = useDestinationPageHotelsMapStore()
|
const { activeMarker: activeHotelId } = useDestinationPageHotelsMapStore()
|
||||||
const isMapView = useMemo(
|
const isMapView = useMemo(
|
||||||
() => searchParams.get("view") === "map",
|
() => searchParams.get("view") === "map",
|
||||||
[searchParams]
|
[searchParams]
|
||||||
@@ -169,6 +169,7 @@ export default function Map({
|
|||||||
defaultCoordinates={defaultCoordinates}
|
defaultCoordinates={defaultCoordinates}
|
||||||
defaultZoom={defaultZoom}
|
defaultZoom={defaultZoom}
|
||||||
fitBounds={!activeHotel}
|
fitBounds={!activeHotel}
|
||||||
|
gestureHandling="greedy"
|
||||||
>
|
>
|
||||||
<MapContent
|
<MapContent
|
||||||
geojson={geoJson}
|
geojson={geoJson}
|
||||||
|
|||||||
@@ -17,3 +17,9 @@
|
|||||||
right: var(--Spacing-x2);
|
right: var(--Spacing-x2);
|
||||||
bottom: var(--Spacing-x2);
|
bottom: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1366px) {
|
||||||
|
.mapWrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ export function useSupercluster<T extends GeoJsonProperties>(
|
|||||||
return clusterer.getClusters(bbox, zoom)
|
return clusterer.getClusters(bbox, zoom)
|
||||||
}, [version, clusterer, bbox, zoom])
|
}, [version, clusterer, bbox, zoom])
|
||||||
|
|
||||||
|
function getClusterZoom(clusterId: number) {
|
||||||
|
if (!clusterer || version === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusterer.getClusterExpansionZoom(clusterId)
|
||||||
|
}
|
||||||
|
|
||||||
// retrieve the hotel ids included in the cluster
|
// retrieve the hotel ids included in the cluster
|
||||||
const containedHotels = clusters.map((cluster) => {
|
const containedHotels = clusters.map((cluster) => {
|
||||||
if (cluster.properties?.cluster && typeof cluster.id === "number") {
|
if (cluster.properties?.cluster && typeof cluster.id === "number") {
|
||||||
@@ -47,5 +55,6 @@ export function useSupercluster<T extends GeoJsonProperties>(
|
|||||||
return {
|
return {
|
||||||
clusters,
|
clusters,
|
||||||
containedHotels,
|
containedHotels,
|
||||||
|
getClusterZoom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
|
|
||||||
interface DestinationPageHotelsMapState {
|
interface DestinationPageHotelsMapState {
|
||||||
hoveredHotel: string | null
|
hoveredMarker: string | null
|
||||||
activeHotel: string | null
|
activeMarker: string | null
|
||||||
setHoveredHotel: (hotelId: string | null) => void
|
setHoveredMarker: (hotelId: string | null) => void
|
||||||
setActiveHotel: (hotelId: string | null) => void
|
setActiveMarker: (hotelId: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDestinationPageHotelsMapStore =
|
export const useDestinationPageHotelsMapStore =
|
||||||
create<DestinationPageHotelsMapState>((set) => ({
|
create<DestinationPageHotelsMapState>((set) => ({
|
||||||
hoveredHotel: null,
|
hoveredMarker: null,
|
||||||
activeHotel: null,
|
activeMarker: null,
|
||||||
setHoveredHotel: (hotelId) => set({ hoveredHotel: hotelId }),
|
setHoveredMarker: (hotelId) => set({ hoveredMarker: hotelId }),
|
||||||
setActiveHotel: (hotelId) => set({ activeHotel: hotelId }),
|
setActiveMarker: (hotelId) => set({ activeMarker: hotelId }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user