feat(SW-1940): Added functionality to see hotel on map from city pages

Approved-by: Fredrik Thorsson
This commit is contained in:
Erik Tiekstra
2025-03-17 13:18:38 +00:00
parent 05addfa0bb
commit c5ad3cba34
10 changed files with 73 additions and 38 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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" />

View File

@@ -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") {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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()

View File

@@ -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}

View File

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