Skip to main content

Map Components

The template provides a provider-abstracted mapping system supporting both Mapbox and Google Maps. UI components live in components/maps/ while the provider abstraction resides in lib/maps/.

Architecture

components/maps/           # React UI components
map.tsx # Main interactive map
location-picker.tsx # Address search + map picker for forms
map-marker.tsx # Marker rendering (internal + standalone display)
map-cluster.tsx # Cluster display, list, and element factory
map-item-popup.tsx # Item preview popup + standalone card
map-error-boundary.tsx # Error boundary for map failures
index.ts # Barrel export with type re-exports

lib/maps/ # Provider abstraction layer
providers/
map-provider.interface.ts # IMapProvider, IMapInstance, IMarkerInstance, etc.
google-map-provider.ts # Google Maps implementation
mapbox-map-provider.ts # Mapbox GL JS implementation
types.ts # Shared types (Coordinates, MapBounds, etc.)

All components are exported from components/maps/index.ts:

export { Map } from './map';
export { MapErrorBoundary } from './map-error-boundary';
export { MapMarkerInternal, MapMarkerDisplay } from './map-marker';
export { ClusterDisplay, ClusterList, createClusterElement } from './map-cluster';
export { MapItemPopup, MapItemCard } from './map-item-popup';
export { LocationPicker } from './location-picker';

Map Component

The main Map component (components/maps/map.tsx) renders an interactive map with automatic provider detection, marker display, optional clustering, and fullscreen toggle.

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

Props

PropTypeDefaultDescription
markersMapMarkerData[][]Array of markers to display
centerCoordinatesSettings defaultMap center (latitude/longitude)
zoomnumber12Initial zoom level
heightnumber or string400Map container height
widthnumber or string'100%'Map container width
controlsobjectZoom + fullscreenShow/hide zoom, fullscreen, scale controls
enableClusteringbooleantrueGroup nearby markers into clusters
clusterOptionsClusterOptions--Radius, maxZoom, minPoints for clustering
isLoadingbooleanfalseShow loading overlay
isDisabledbooleanfalseShow disabled state placeholder
errorstring or nullnullDisplay error state
onMarkerClick(marker) => void--Called when a marker is clicked
onClusterClick(cluster) => void--Called when a cluster is clicked
onViewportChange(viewport) => void--Called on pan/zoom with center, zoom, bounds
onReady() => void--Called when map finishes loading
onError(error) => void--Called on map initialization errors
ariaLabelstring'Interactive map'Accessibility label

State Management

The Map component handles three visual states:

  • Disabled -- renders a placeholder with a map pin icon and "Map is disabled" text. Triggered when isDisabled is true or map settings are disabled.
  • Error -- renders an alert with the error message and an alert icon.
  • Loading -- shows a spinner overlay while the provider initializes.

Clustering

When enableClustering is true and markers are provided, the component uses the provider's createClusterer method:

clustererRef.current = provider.createClusterer(mapInstance, {
radius: clusterOptions?.radius ?? 60,
maxZoom: clusterOptions?.maxZoom ?? 16,
minPoints: clusterOptions?.minPoints ?? 2
}, (cluster) => {
onClusterClickRef.current?.({
id: `cluster-${cluster.coordinates.latitude}-${cluster.coordinates.longitude}`,
coordinates: cluster.coordinates,
count: cluster.markerIds.length,
markerIds: cluster.markerIds,
expansionZoom: cluster.expansionZoom
});
});

LocationPicker

The LocationPicker component (components/maps/location-picker.tsx) provides a full location editing experience for forms.

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

Features

  • Address autocomplete using the configured map provider
  • Map preview with a draggable marker
  • "Use My Location" button for browser geolocation
  • Service area dropdown with four levels: Local, Regional, National, Global
  • Remote/online service checkbox for services without physical locations
  • Form integration with value/onChange pattern and error display

Props

PropTypeDefaultDescription
valueLocationPickerValue--Current location data
onChange(value) => void--Called when location changes
errorsobject--Validation errors for address, coordinates, serviceArea
showMapbooleantrueShow the map preview
showServiceAreabooleantrueShow service area dropdown
showRemoteOptionbooleantrueShow the remote service checkbox
mapHeightnumber or string200Height of the map preview
isDisabledbooleanfalseDisable all inputs

LocationPickerValue

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

MapMarker Components

MapMarkerInternal

Used internally by the Map component. Creates a provider-native marker on an existing map instance. This component renders null -- the actual marker is rendered by the map library.

MapMarkerDisplay

A standalone React component for displaying marker-like UI outside of maps (legends, lists, previews):

<MapMarkerDisplay
icon="/images/tool.png"
title="My Tool"
category="Productivity"
size="md"
isSelected={selectedId === 'my-tool'}
onClick={() => handleSelect('my-tool')}
/>

Supports three sizes (sm, md, lg) with appropriate icon dimensions.

Cluster Components

Located in components/maps/map-cluster.tsx:

ClusterDisplay

A circular badge showing the marker count within a cluster. Color changes based on count thresholds:

CountColorSize
Under 10Bluew-8 h-8
10--49Yelloww-10 h-10
50+Pinkw-12 h-12

Counts over 99 are displayed as "99+".

ClusterList

Renders a vertical list of clusters with count badges and expansion zoom information:

<ClusterList
clusters={clusterData}
onClusterClick={(cluster) => map.setZoom(cluster.expansionZoom)}
selectedClusterId={selectedId}
/>

createClusterElement

Factory function that creates an HTMLElement for use as a custom marker in map providers:

const element = createClusterElement(42);
// Returns a styled div with "42" text, appropriate size/color

MapItemPopup

The MapItemPopup component (components/maps/map-item-popup.tsx) displays an item preview when a marker is clicked.

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

Features

  • Item icon, name, and category display
  • Truncated description preview (120 characters max)
  • "View Details" link to the item page
  • Close button with keyboard support (Escape key)
  • Click-outside-to-close behavior
  • Focus management: close button is auto-focused when popup opens
  • ARIA role="dialog" with translated label

MapItemCard

A standalone card component for item display outside of maps:

<MapItemCard
slug="example"
name="Example Item"
icon="/images/icon.png"
category="Tools"
description="A great tool for productivity"
locale="en"
/>

Renders as a Link by default, or as a button when an onClick handler is provided.

MapErrorBoundary

A React error boundary specifically for map components (components/maps/map-error-boundary.tsx):

<MapErrorBoundary
onRetry={() => window.location.reload()}
fallback={<div>Custom fallback UI</div>}
>
<Map markers={items} />
</MapErrorBoundary>

Catches rendering errors and displays a friendly fallback with a "Try Again" button.

Provider Interface

The IMapProvider interface (lib/maps/providers/map-provider.interface.ts) defines the contract both providers must implement:

interface IMapProvider {
readonly name: 'mapbox' | 'google';
isLoaded(): boolean;
loadScript(): Promise<void>;
createMap(container: HTMLElement, options: MapCreateOptions): Promise<IMapInstance>;
createMarker(map: IMapInstance, options: MarkerCreateOptions): IMarkerInstance;
createClusterer(map: IMapInstance, options: ClusterOptions, onClusterClick?): IClustererInstance;
createAutocomplete(input: HTMLInputElement, onSelect): IAutocompleteInstance;
getStyleUrl(style: MapStyle): string;
isConfigured(): boolean;
}

Instance Interfaces

InterfaceMethods
IMapInstancesetCenter, setZoom, getCenter, getZoom, getBounds, fitBounds, resize, on, off, destroy
IMarkerInstancesetPosition, setDraggable, getPosition, show, hide, remove, onClick, onDragEnd
IClustererInstanceaddMarkers, removeMarkers, clearMarkers, refresh, destroy
IAutocompleteInstanceclear, destroy

Shared Types

Key types from lib/maps/types.ts:

interface Coordinates {
latitude: number;
longitude: number;
}

interface MapBounds {
north: number;
south: number;
east: number;
west: number;
}

type ServiceArea = 'local' | 'regional' | 'national' | 'global';

interface MapMarkerData {
id: string;
coordinates: Coordinates;
title: string;
slug: string;
icon?: string;
category?: string;
description?: string;
}
PathDescription
components/maps/index.tsBarrel export for all map components and types
components/maps/map.tsxMain interactive map component
components/maps/location-picker.tsxForm location picker with autocomplete
components/maps/map-marker.tsxInternal and display marker components
components/maps/map-cluster.tsxCluster display, list, and element factory
components/maps/map-item-popup.tsxItem popup and standalone card
components/maps/map-error-boundary.tsxMap-specific error boundary
lib/maps/providers/map-provider.interface.tsProvider interface contract
lib/maps/providers/mapbox-map-provider.tsMapbox GL JS implementation
lib/maps/providers/google-map-provider.tsGoogle Maps implementation
lib/maps/types.tsShared map type definitions
hooks/use-map-provider.tsHook for accessing the map provider instance
hooks/use-location-settings.tsHook for map/location settings
hooks/use-geolocation.tsHook for browser geolocation API