Skip to content

Instantly share code, notes, and snippets.

@cristoferespindola
Created February 14, 2024 18:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cristoferespindola/42a6f2d148db58b68b00651071fd9b1c to your computer and use it in GitHub Desktop.
Save cristoferespindola/42a6f2d148db58b68b00651071fd9b1c to your computer and use it in GitHub Desktop.
Custom Yext MapboxMap
// Custom Yext MapboxMap component to render a map with markers to show result locations using Mapbox GL.
// extracted from https://github.com/yext/search-ui-react/blob/main/src/components/MapboxMap.tsx
// and modified to fit the needs of the project.
// To implement zoom controls, the zoomControls prop was added to the CustomMapboxMap component.
import React, { useRef, useEffect } from 'react'
import mapboxgl from 'mapbox-gl'
import { Result, useSearchState } from '@yext/search-headless-react'
import ReactDOM from 'react-dom'
import { MapboxMapProps } from '@yext/search-ui-react'
import { Coordinate } from '@yext/sites-components/dist/map/coordinate'
import { useDebouncedFunction } from '@/hooks/useDebouncedFunction'
export interface CustomMapboxMapProps<T> extends MapboxMapProps<T> {
/**
* Custom zoom controls for the map.
* Whether to show zoom controls on the map.
* */
zoomControls?: boolean
}
export function CustomMapboxMap<T>({
mapboxAccessToken,
mapboxOptions,
PinComponent,
getCoordinate = getDefaultCoordinate,
onDrag,
zoomControls,
}: CustomMapboxMapProps<T>): JSX.Element {
useEffect(() => {
mapboxgl.accessToken = mapboxAccessToken
}, [mapboxAccessToken])
const mapContainer = useRef<HTMLDivElement>(null)
const map = useRef<mapboxgl.Map | null>(null)
const markers = useRef<mapboxgl.Marker[]>([])
const locationResults = useSearchState((state) => state.vertical.results) as Result<T>[]
const onDragDebounced = useDebouncedFunction(onDrag, 100)
useEffect(() => {
if (mapContainer.current && !map.current) {
const options: mapboxgl.MapboxOptions = {
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v11',
center: [-74.005371, 40.741611],
zoom: 9,
...mapboxOptions,
}
map.current = new mapboxgl.Map(options)
const mapbox = map.current
if (zoomControls) {
mapbox.addControl(new mapboxgl.NavigationControl(), 'top-right')
}
mapbox.resize()
if (onDragDebounced) {
mapbox.on('drag', () => {
onDragDebounced(mapbox.getCenter(), mapbox.getBounds())
})
}
}
}, [mapboxOptions, onDragDebounced])
useEffect(() => {
markers.current.forEach((marker) => marker.remove())
markers.current = []
const mapbox = map.current
if (mapbox && locationResults?.length > 0) {
const bounds = new mapboxgl.LngLatBounds()
locationResults.forEach((result, i) => {
const markerLocation = getCoordinate(result)
if (markerLocation) {
const { latitude, longitude } = markerLocation
const el = document.createElement('div')
const markerOptions: mapboxgl.MarkerOptions = {}
if (PinComponent) {
ReactDOM.render(<PinComponent index={i} mapbox={mapbox} result={result} />, el)
markerOptions.element = el
}
const marker = new mapboxgl.Marker(markerOptions)
.setLngLat({ lat: latitude, lng: longitude })
.addTo(mapbox)
markers.current.push(marker)
bounds.extend([longitude, latitude])
}
})
if (!bounds.isEmpty()) {
mapbox.fitBounds(bounds, {
padding: { top: 50, bottom: 50, left: 50, right: 50 },
maxZoom: 15,
})
}
}
}, [PinComponent, getCoordinate, locationResults])
return <div ref={mapContainer} className="h-full w-full" />
}
function isCoordinate(data: any): data is Coordinate {
return typeof data == 'object' && typeof data?.['latitude'] === 'number' && typeof data?.['longitude'] === 'number'
}
function getDefaultCoordinate<T>(result: Result<T>): Coordinate | undefined {
const yextDisplayCoordinate = result.rawData['yextDisplayCoordinate' as keyof T]
if (!yextDisplayCoordinate) {
console.error(
'Unable to use the default "yextDisplayCoordinate" field as the result\'s coordinate to display on map.' +
'\nConsider providing the "getCoordinate" prop to MapboxMap component to fetch the desire coordinate from result.'
)
return undefined
}
if (!isCoordinate(yextDisplayCoordinate)) {
console.error('The default `yextDisplayCoordinate` field from result is not of type "Coordinate".')
return undefined
}
return yextDisplayCoordinate
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment