Skip to main content

Map Integration Guide

The template ships a full-featured map system with components in components/maps/. Maps are provider-agnostic -- the same components work with either Mapbox GL JS or Google Maps depending on your configuration.

Looking to enable the listing Map view? This guide is for developers integrating individual map components. If you just want to flip the directory listing into a map (with a synchronised sidebar of cards and a dedicated /map route), see the Map View feature page and Spec 017 — Map View.

Component Inventory

ExportFileDescription
Mapmap.tsxMain interactive map with markers and clustering
LocationPickerlocation-picker.tsxAddress input with autocomplete, map preview, and geolocation
MapMarkerInternalmap-marker.tsxRenders a marker on an existing map instance
MapMarkerDisplaymap-marker.tsxStandalone marker preview for lists/legends
ClusterDisplaymap-cluster.tsxCluster badge for use outside the map
ClusterListmap-cluster.tsxCluster list with click handlers
createClusterElementmap-cluster.tsxCreates an HTML element for custom cluster markers
MapItemPopupmap-item-popup.tsxItem preview popup anchored to a marker
MapItemCardmap-item-popup.tsxStandalone item card for sidebars
MapErrorBoundarymap-error-boundary.tsxError boundary with retry button

All components and types are re-exported from components/maps/index.ts.

The Map Component

The primary component for displaying an interactive map:

import { Map } from '@/components/maps';

<Map
markers={items}
center={{ latitude: 40.7128, longitude: -74.0060 }}
zoom={12}
onMarkerClick={(marker) => console.log('Clicked:', marker)}
/>

Props

PropTypeDefaultDescription
markersMapMarkerData[][]Array of marker data objects
centerCoordinatesFrom settingsMap center point
zoomnumber12Initial zoom level
heightnumber | string400Map container height
widthnumber | string'100%'Map container width
controlsobjectSee belowControl visibility flags
enableClusteringbooleantrueGroup nearby markers into clusters
clusterOptionsobject--Cluster radius, maxZoom, minPoints
isLoadingbooleanfalseShow loading overlay externally
isDisabledbooleanfalseRender disabled state
errorstring | nullnullDisplay error message
onMarkerClick(marker) => void--Called when a marker is clicked
onClusterClick(cluster) => void--Called when a cluster is clicked
onViewportChange(viewport) => void--Called after pan/zoom
onReady() => void--Fires when map initialisation completes
onError(error) => void--Fires on map errors
ariaLabelstring'Interactive map'Accessibility label

Controls Object

{
showZoomControls: true,
showFullscreenControl: true,
showScaleControl: false,
}

State Management

The Map component uses the provider abstraction layer (useMapProviderInstance) which returns the active provider and its loading/error states. Center and zoom are tracked internally with useEffect hooks that update when props change.

Clustering

When enableClustering is true (default), markers are grouped using the provider's clustering implementation. Cluster appearance is based on marker count:

CountSizeColour
1 -- 9Small (w-8 h-8)Blue
10 -- 49Medium (w-10 h-10)Yellow
50+Large (w-12 h-12)Pink

LocationPicker

A form-integrated component for selecting locations with address autocomplete:

import { LocationPicker } from '@/components/maps';

<LocationPicker
value={formData.location}
onChange={(location) => setFormData({ ...formData, location })}
showServiceArea
showRemoteOption
/>

Features

  • Address autocomplete using the configured map provider's geocoding API.
  • Map preview with a draggable marker for fine-tuning position.
  • "Use My Location" button that calls the browser geolocation API.
  • Service area dropdown with four options: Local, Regional, National, Global.
  • Remote/online checkbox for services without a physical location.
  • Error display for address and coordinate validation errors.

LocationPicker Props

PropTypeDefaultDescription
valueLocationPickerValue--Current location state
onChange(value) => void--Called on any change
errorsobject--Validation errors for address/coordinates
showMapbooleantrueShow the map preview
showServiceAreabooleantrueShow the service area dropdown
showRemoteOptionbooleantrueShow the remote checkbox
mapHeightnumber | string200Height of the map preview
isDisabledbooleanfalseDisable all interactions

LocationPickerValue

interface LocationPickerValue {
address?: string;
latitude?: number;
longitude?: number;
serviceArea?: 'local' | 'regional' | 'national' | 'global';
isRemote?: boolean;
}

MapErrorBoundary

Wrap map components in MapErrorBoundary to catch rendering errors:

import { MapErrorBoundary } from '@/components/maps';

<MapErrorBoundary onRetry={() => refetch()}>
<Map markers={items} />
</MapErrorBoundary>

If the map throws, the boundary shows an error message with a "Try Again" button. You can provide a custom fallback node instead.

MapItemPopup

Displays item details in a popup anchored to a map marker:

<MapItemPopup
item={{ slug: 'example', name: 'Example', category: 'Tools' }}
isOpen={isPopupOpen}
position={{ latitude: 40.7128, longitude: -74.0060 }}
onClose={() => setIsPopupOpen(false)}
locale="en"
/>

The popup includes:

  • Item icon, name, and category badge
  • Truncated description (max 120 characters)
  • "View Details" link to the item page
  • Close button with keyboard (Escape) and click-outside support
  • Focus management for accessibility

Hooks

useMapProviderInstance

Returns the active map provider, loading state, and any initialisation error. Used internally by Map and LocationPicker.

useLocationSettings

Reads location configuration from the SettingsProvider context:

import { useLocationSettings } from '@/hooks/use-location-settings';

const { settings } = useLocationSettings();
// settings.enabled, settings.provider, settings.defaultCenter, etc.

useGeolocation

Browser geolocation API wrapper used by LocationPicker for the "Use My Location" feature. Returns { isLoading, requestLocation, permission }.