Created
February 14, 2024 18:47
-
-
Save cristoferespindola/42a6f2d148db58b68b00651071fd9b1c to your computer and use it in GitHub Desktop.
Custom Yext MapboxMap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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