Files
web/packages/design-system/lib/components/Map/InteractiveMap/index.tsx
Erik Tiekstra 4ec1e85d84 Feat/BOOK-293 button adjustments
* feat(BOOK-293): Adjusted padding of the buttons to match Figma design
* feat(BOOK-293): Updated variants for IconButton
* feat(BOOK-113): Updated focus indicators on buttons and added default focus ring color
* feat(BOOK-293): Replaced buttons inside booking widget

Approved-by: Christel Westerberg
2025-12-15 07:05:31 +00:00

174 lines
4.7 KiB
TypeScript

'use client'
import { Map, type MapProps, useMap } from '@vis.gl/react-google-maps'
import { useEffect, useState } from 'react'
import { useIntl } from 'react-intl'
import { IconButton } from '../../IconButton'
import { MaterialIcon } from '../../Icons/MaterialIcon'
import { HOTEL_PAGE, MAP_RESTRICTIONS } from '../mapConstants'
import { useZoomControls } from '@scandic-hotels/common/hooks/map/useZoomControls'
import { HotelListingMapContent } from './HotelListingMapContent'
import PoiMapMarkers from './PoiMapMarkers'
import styles from './interactiveMap.module.css'
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
import { Lang } from '@scandic-hotels/common/constants/language'
import { HotelPin, MarkerInfo, PointOfInterest } from '../types'
export type InteractiveMapProps = {
lang: Lang
coordinates: {
lat: number
lng: number
}
activePoi?: string | null
hotelPins?: HotelPin[]
pointsCurrency?: CurrencyEnum
pointsOfInterest?: PointOfInterest[]
markerInfo?: MarkerInfo
mapId: string
closeButton: React.ReactNode
fitBounds?: boolean
hoveredHotelPin?: string | null
activeHotelPin?: string | null
isUserLoggedIn: boolean
onTilesLoaded?: () => void
onActivePoiChange?: (poi: string | null) => void
onClickHotel?: (hotelId: string) => void
/**
* Called when a hotel pin is hovered.
* @param args when null, it means the hover has ended
* @returns
*/
onHoverHotelPin?: (
args: { hotelName: string; hotelId: string } | null
) => void
/**
* Called when a hotel pin is activated.
* @param args when null, it means nothing is active
* @returns
*/
onSetActiveHotelPin?: (
args: { hotelName: string; hotelId: string } | null
) => void
}
export function InteractiveMap({
lang,
coordinates,
pointsOfInterest,
activePoi,
hotelPins,
mapId,
closeButton,
markerInfo,
fitBounds = true,
hoveredHotelPin,
activeHotelPin,
isUserLoggedIn,
pointsCurrency,
onClickHotel,
onHoverHotelPin,
onSetActiveHotelPin,
onTilesLoaded,
onActivePoiChange,
}: InteractiveMapProps) {
const intl = useIntl()
const map = useMap()
const [hasInitializedBounds, setHasInitializedBounds] = useState(false)
const { zoomIn, zoomOut, isMaxZoom, isMinZoom } = useZoomControls(HOTEL_PAGE)
const mapOptions: MapProps = {
defaultZoom: HOTEL_PAGE.DEFAULT_ZOOM,
minZoom: HOTEL_PAGE.MIN_ZOOM,
maxZoom: HOTEL_PAGE.MAX_ZOOM,
defaultCenter: coordinates,
disableDefaultUI: true,
clickableIcons: false,
mapId,
gestureHandling: 'greedy',
restriction: MAP_RESTRICTIONS,
}
useEffect(() => {
if (map && hotelPins?.length && !hasInitializedBounds) {
if (fitBounds) {
const bounds = new google.maps.LatLngBounds()
hotelPins.forEach((marker) => {
bounds.extend(marker.coordinates)
})
map.fitBounds(bounds, 100)
}
setHasInitializedBounds(true)
}
}, [map, fitBounds, hotelPins, hasInitializedBounds])
return (
<div className={styles.mapContainer}>
<Map {...mapOptions} onTilesLoaded={onTilesLoaded}>
{hotelPins && (
<HotelListingMapContent
lang={lang}
isUserLoggedIn={isUserLoggedIn}
hotelPins={hotelPins}
setActiveHotel={onSetActiveHotelPin}
setHoveredHotel={onHoverHotelPin}
activeHotel={activeHotelPin}
hoveredHotel={hoveredHotelPin}
onClickHotel={onClickHotel}
pointsCurrency={pointsCurrency}
/>
)}
{pointsOfInterest && markerInfo && (
<PoiMapMarkers
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
onActivePoiChange={onActivePoiChange}
activePoi={activePoi}
markerInfo={markerInfo}
/>
)}
</Map>
<div className={styles.ctaButtons}>
{closeButton}
<div className={styles.zoomButtons}>
<IconButton
variant="Elevated"
className={styles.zoomButton}
onClick={zoomOut}
aria-label={intl.formatMessage({
id: 'map.zoomOut',
defaultMessage: 'Zoom out',
})}
isDisabled={isMinZoom}
>
<MaterialIcon icon="remove" color="CurrentColor" />
</IconButton>
<IconButton
variant="Elevated"
className={styles.zoomButton}
onClick={zoomIn}
aria-label={intl.formatMessage({
id: 'map.zoomIn',
defaultMessage: 'Zoom in',
})}
isDisabled={isMaxZoom}
>
<MaterialIcon icon="add" color="CurrentColor" />
</IconButton>
</div>
</div>
</div>
)
}