feat(SW-1940): Added functionality to see hotel on map from city pages
Approved-by: Fredrik Thorsson
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, clickedHotel } = useDestinationPageHotelsMapStore()
|
const { setHoveredHotel, activeHotel } = useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (clickedHotel === hotel.operaId) {
|
if (activeHotel === hotel.operaId) {
|
||||||
const element = itemRef.current
|
const element = itemRef.current
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
@@ -45,7 +45,7 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [clickedHotel, hotel.operaId])
|
}, [activeHotel, hotel.operaId])
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
if (hotel.operaId) {
|
if (hotel.operaId) {
|
||||||
@@ -62,7 +62,7 @@ export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
|||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
className={`${styles.hotelListItem} ${clickedHotel === hotel.operaId ? styles.activeCard : ""}`}
|
className={`${styles.hotelListItem} ${activeHotel === hotel.operaId ? styles.activeCard : ""}`}
|
||||||
>
|
>
|
||||||
<div className={styles.imageWrapper}>
|
<div className={styles.imageWrapper}>
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ interface MapCardCarouselProps {
|
|||||||
export default function HotelCardCarousel({
|
export default function HotelCardCarousel({
|
||||||
visibleHotels,
|
visibleHotels,
|
||||||
}: MapCardCarouselProps) {
|
}: MapCardCarouselProps) {
|
||||||
const { clickedHotel } = useDestinationPageHotelsMapStore()
|
const { activeHotel } = useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
const selectedHotelIdx = visibleHotels.findIndex(
|
const selectedHotelIdx = visibleHotels.findIndex(
|
||||||
({ hotel }) => hotel.operaId === clickedHotel
|
({ hotel }) => hotel.operaId === activeHotel
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -35,7 +35,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]: !clickedHotel,
|
[styles.noActiveHotel]: !activeHotel,
|
||||||
})}
|
})}
|
||||||
tripadvisorRating={hotel.ratings?.tripAdvisor.rating}
|
tripadvisorRating={hotel.ratings?.tripAdvisor.rating}
|
||||||
hotelName={hotel.name}
|
hotelName={hotel.name}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useParams } from "next/navigation"
|
||||||
|
import { useEffect, useState } 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 { ChevronRightSmallIcon, TripAdvisorIcon } from "@/components/Icons"
|
import { ChevronRightSmallIcon, TripAdvisorIcon } from "@/components/Icons"
|
||||||
import HotelLogo from "@/components/Icons/Logos"
|
import HotelLogo from "@/components/Icons/Logos"
|
||||||
@@ -29,8 +33,17 @@ export default function HotelListingItem({
|
|||||||
url,
|
url,
|
||||||
}: HotelListingItemProps) {
|
}: HotelListingItemProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const params = useParams()
|
||||||
|
const { setActiveHotel } = 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)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.searchParams.set("view", "map")
|
||||||
|
setMapUrl(url.toString())
|
||||||
|
}, [params, hotel.name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.container}>
|
<article className={styles.container}>
|
||||||
@@ -90,10 +103,18 @@ export default function HotelListingItem({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<Button intent="text" variant="icon" theme="base">
|
{mapUrl && (
|
||||||
{intl.formatMessage({ id: "See on map" })}
|
<Button intent="text" variant="icon" theme="base" asChild>
|
||||||
<ChevronRightSmallIcon />
|
<Link
|
||||||
</Button>
|
href={mapUrl}
|
||||||
|
scroll={true}
|
||||||
|
onClick={() => setActiveHotel(hotel.operaId)}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "See on map" })}
|
||||||
|
<ChevronRightSmallIcon />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{url && (
|
{url && (
|
||||||
<>
|
<>
|
||||||
<Divider variant="horizontal" color="primaryLightSubtle" />
|
<Divider variant="horizontal" color="primaryLightSubtle" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { cx } from "class-variance-authority"
|
import { cx } from "class-variance-authority"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useCallback, useState } from "react"
|
import { useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
@@ -44,11 +44,11 @@ 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 { setClickedHotel } = useDestinationPageHotelsMapStore()
|
const { setActiveHotel } = useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
function handleClose() {
|
||||||
setClickedHotel(null)
|
setActiveHotel(null)
|
||||||
}, [setClickedHotel])
|
}
|
||||||
|
|
||||||
function Wrapper({ children }: React.PropsWithChildren) {
|
function Wrapper({ children }: React.PropsWithChildren) {
|
||||||
if (type === "dialog") {
|
if (type === "dialog") {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface DynamicMapProps {
|
|||||||
mapId: string
|
mapId: string
|
||||||
defaultCoordinates: google.maps.LatLngLiteral | null
|
defaultCoordinates: google.maps.LatLngLiteral | null
|
||||||
defaultZoom: number
|
defaultZoom: number
|
||||||
|
fitBounds?: boolean
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export default function DynamicMap({
|
|||||||
mapId,
|
mapId,
|
||||||
defaultCoordinates,
|
defaultCoordinates,
|
||||||
defaultZoom,
|
defaultZoom,
|
||||||
|
fitBounds = true,
|
||||||
onClose,
|
onClose,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<DynamicMapProps>) {
|
}: PropsWithChildren<DynamicMapProps>) {
|
||||||
@@ -40,7 +42,7 @@ export default function DynamicMap({
|
|||||||
const map = useMap()
|
const map = useMap()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map) {
|
if (map && fitBounds) {
|
||||||
if (markers.length) {
|
if (markers.length) {
|
||||||
const bounds = new google.maps.LatLngBounds()
|
const bounds = new google.maps.LatLngBounds()
|
||||||
markers.forEach((marker) => {
|
markers.forEach((marker) => {
|
||||||
@@ -51,7 +53,7 @@ export default function DynamicMap({
|
|||||||
map.setCenter(defaultCoordinates)
|
map.setCenter(defaultCoordinates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [map, markers, defaultCoordinates])
|
}, [map, markers, defaultCoordinates, fitBounds])
|
||||||
|
|
||||||
useHandleKeyUp((event: KeyboardEvent) => {
|
useHandleKeyUp((event: KeyboardEvent) => {
|
||||||
if (event.key === "Escape" && onClose) {
|
if (event.key === "Escape" && onClose) {
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ export default function ClusterMarker({
|
|||||||
onMarkerClick,
|
onMarkerClick,
|
||||||
hotelIds,
|
hotelIds,
|
||||||
}: ClusterMarkerProps) {
|
}: ClusterMarkerProps) {
|
||||||
const { hoveredHotel, clickedHotel } = useDestinationPageHotelsMapStore()
|
const { hoveredHotel, activeHotel } = useDestinationPageHotelsMapStore()
|
||||||
const isActive =
|
const isActive =
|
||||||
hotelIds.includes(Number(hoveredHotel)) ||
|
hotelIds.includes(Number(hoveredHotel)) ||
|
||||||
hotelIds.includes(Number(clickedHotel))
|
hotelIds.includes(Number(activeHotel))
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (onMarkerClick) {
|
if (onMarkerClick) {
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ interface MarkerProps {
|
|||||||
export default function Marker({ position, properties }: MarkerProps) {
|
export default function Marker({ position, properties }: MarkerProps) {
|
||||||
const [markerRef] = useAdvancedMarkerRef()
|
const [markerRef] = useAdvancedMarkerRef()
|
||||||
|
|
||||||
const { setHoveredHotel, setClickedHotel, hoveredHotel, clickedHotel } =
|
const { setHoveredHotel, setActiveHotel, hoveredHotel, activeHotel } =
|
||||||
useDestinationPageHotelsMapStore()
|
useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setClickedHotel(properties.id)
|
setActiveHotel(properties.id)
|
||||||
trackMapClick(properties.name)
|
trackMapClick(properties.name)
|
||||||
}, [setClickedHotel, properties])
|
}, [setActiveHotel, properties])
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
setHoveredHotel(properties.id)
|
setHoveredHotel(properties.id)
|
||||||
@@ -41,7 +41,7 @@ export default function Marker({ position, properties }: MarkerProps) {
|
|||||||
}, [setHoveredHotel])
|
}, [setHoveredHotel])
|
||||||
|
|
||||||
const isHovered = hoveredHotel === properties.id
|
const isHovered = hoveredHotel === properties.id
|
||||||
const isActive = clickedHotel === properties.id
|
const isActive = activeHotel === properties.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdvancedMarker
|
<AdvancedMarker
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function MapContent({
|
|||||||
geojson,
|
geojson,
|
||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
}: MapContentProps) {
|
}: MapContentProps) {
|
||||||
const { setClickedHotel, clickedHotel } = useDestinationPageHotelsMapStore()
|
const { setActiveHotel, activeHotel } = useDestinationPageHotelsMapStore()
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
|
|
||||||
const { clusters, containedHotels } = useSupercluster<MarkerProperties>(
|
const { clusters, containedHotels } = useSupercluster<MarkerProperties>(
|
||||||
@@ -47,11 +47,11 @@ export default function MapContent({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map?.addListener("click", () => {
|
map?.addListener("click", () => {
|
||||||
if (clickedHotel) {
|
if (activeHotel) {
|
||||||
setClickedHotel(null)
|
setActiveHotel(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [clickedHotel, map, setClickedHotel])
|
}, [activeHotel, map, setActiveHotel])
|
||||||
|
|
||||||
function handleClusterClick(position: google.maps.LatLngLiteral) {
|
function handleClusterClick(position: google.maps.LatLngLiteral) {
|
||||||
const currentZoom = map && map.getZoom()
|
const currentZoom = map && map.getZoom()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Dialog, Modal } from "react-aria-components"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useDestinationDataStore } from "@/stores/destination-data"
|
import { useDestinationDataStore } from "@/stores/destination-data"
|
||||||
|
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
|
||||||
|
|
||||||
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
|
||||||
import { ChevronLeftSmallIcon } from "@/components/Icons"
|
import { ChevronLeftSmallIcon } from "@/components/Icons"
|
||||||
@@ -47,10 +48,14 @@ 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 isMapView = useMemo(
|
const isMapView = useMemo(
|
||||||
() => searchParams.get("view") === "map",
|
() => searchParams.get("view") === "map",
|
||||||
[searchParams]
|
[searchParams]
|
||||||
)
|
)
|
||||||
|
const activeHotel = hotels.find(
|
||||||
|
({ hotel }) => hotel.operaId === activeHotelId
|
||||||
|
)
|
||||||
const rootDiv = useRef<HTMLDivElement | null>(null)
|
const rootDiv = useRef<HTMLDivElement | null>(null)
|
||||||
const [mapHeight, setMapHeight] = useState("0px")
|
const [mapHeight, setMapHeight] = useState("0px")
|
||||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||||
@@ -65,14 +70,20 @@ export default function Map({
|
|||||||
|
|
||||||
const markers = getHotelMapMarkers(hotels)
|
const markers = getHotelMapMarkers(hotels)
|
||||||
const geoJson = mapMarkerDataToGeoJson(markers)
|
const geoJson = mapMarkerDataToGeoJson(markers)
|
||||||
const defaultCoordinates = defaultLocation
|
const defaultCoordinates = activeHotel
|
||||||
? {
|
? {
|
||||||
lat: defaultLocation.latitude,
|
lat: activeHotel.hotel.location.latitude,
|
||||||
lng: defaultLocation.longitude,
|
lng: activeHotel.hotel.location.longitude,
|
||||||
}
|
}
|
||||||
: null
|
: defaultLocation
|
||||||
const defaultZoom =
|
? {
|
||||||
defaultLocation?.default_zoom ?? (pageType === "city" ? 10 : 3)
|
lat: defaultLocation.latitude,
|
||||||
|
lng: defaultLocation.longitude,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
const defaultZoom = activeHotel
|
||||||
|
? 15
|
||||||
|
: (defaultLocation?.default_zoom ?? (pageType === "city" ? 10 : 3))
|
||||||
|
|
||||||
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
|
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
|
||||||
const handleMapHeight = useCallback(() => {
|
const handleMapHeight = useCallback(() => {
|
||||||
@@ -157,6 +168,7 @@ export default function Map({
|
|||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
defaultCoordinates={defaultCoordinates}
|
defaultCoordinates={defaultCoordinates}
|
||||||
defaultZoom={defaultZoom}
|
defaultZoom={defaultZoom}
|
||||||
|
fitBounds={!activeHotel}
|
||||||
>
|
>
|
||||||
<MapContent
|
<MapContent
|
||||||
geojson={geoJson}
|
geojson={geoJson}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { create } from "zustand"
|
|||||||
|
|
||||||
interface DestinationPageHotelsMapState {
|
interface DestinationPageHotelsMapState {
|
||||||
hoveredHotel: string | null
|
hoveredHotel: string | null
|
||||||
clickedHotel: string | null
|
activeHotel: string | null
|
||||||
setHoveredHotel: (hotelId: string | null) => void
|
setHoveredHotel: (hotelId: string | null) => void
|
||||||
setClickedHotel: (hotelId: string | null) => void
|
setActiveHotel: (hotelId: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDestinationPageHotelsMapStore =
|
export const useDestinationPageHotelsMapStore =
|
||||||
create<DestinationPageHotelsMapState>((set) => ({
|
create<DestinationPageHotelsMapState>((set) => ({
|
||||||
hoveredHotel: null,
|
hoveredHotel: null,
|
||||||
clickedHotel: null,
|
activeHotel: null,
|
||||||
setHoveredHotel: (hotelId) => set({ hoveredHotel: hotelId }),
|
setHoveredHotel: (hotelId) => set({ hoveredHotel: hotelId }),
|
||||||
setClickedHotel: (hotelId) => set({ clickedHotel: hotelId }),
|
setActiveHotel: (hotelId) => set({ activeHotel: hotelId }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user