Skip to main content

Maps & Location Features

The Ever Works template supports interactive maps with a provider abstraction layer for both Google Maps and Mapbox. The system includes geocoding, location picking, marker clustering, and geolocation.

See also: Map View — the listing-level map experience (sidebar + map, header link, dedicated /map route) built on top of these primitives. Spec: 017-map-view. Provider abstraction spec: 011-maps-providers.

Architecture

The lib/maps/ module re-exports all types and providers:

// lib/maps/index.ts
export * from './types';
export * from './providers';

Core Types

Coordinates

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

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

Markers

interface MapMarkerData {
id: string;
coordinates: Coordinates;
title: string;
icon?: string;
category?: string;
slug: string; // Item slug for linking
description?: string;
}

interface MapMarkerWithDistance extends MapMarkerData {
distanceKm?: number; // Distance from reference point
}

Clustering

interface MapClusterData {
id: string;
coordinates: Coordinates;
count: number;
markerIds: string[];
expansionZoom: number;
}

interface ClusterOptions {
radius?: number; // Cluster radius in pixels (default: 60)
maxZoom?: number; // Max zoom for clustering (default: 16)
minZoom?: number; // Min zoom for clustering (default: 0)
minPoints?: number; // Min points per cluster (default: 2)
}

Viewport

interface MapViewport {
center: Coordinates;
zoom: number;
bounds?: MapBounds;
}

MapComponent Props

The main map component accepts a comprehensive set of props:

interface MapComponentProps {
markers?: MapMarkerData[];
center?: Coordinates;
zoom?: number; // 1-20
style?: MapStyle; // 'streets' | 'satellite'
className?: string;
height?: string | number;
width?: string | number;
controls?: MapControlsConfig;
enableClustering?: boolean;
clusterOptions?: ClusterOptions;
isLoading?: boolean;
isDisabled?: boolean;
error?: string | null;
onMarkerClick?: (marker: MapMarkerData) => void;
onClusterClick?: (cluster: MapClusterData) => void;
onViewportChange?: (viewport: MapViewport) => void;
onReady?: () => void;
onError?: (error: Error) => void;
ariaLabel?: string;
}

Map Controls

interface MapControlsConfig {
showZoomControls?: boolean;
showFullscreenControl?: boolean;
showNavigationControl?: boolean;
showScaleControl?: boolean;
}

LocationPicker Component

A form component for selecting locations with address autocomplete, map preview, and service area selection:

interface LocationPickerProps {
value?: LocationPickerValue;
onChange?: (location: LocationPickerValue) => void;
errors?: { address?: string; coordinates?: string; serviceArea?: string };
showMap?: boolean;
showServiceArea?: boolean;
showRemoteOption?: boolean;
mapHeight?: string | number;
isDisabled?: boolean;
isLoading?: boolean;
}

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

Hooks

useMapProvider

Determines the active map provider and its configuration status:

import { useMapProvider } from '@/hooks/use-map-provider';

const {
provider, // 'google' | 'mapbox'
isConfigured, // boolean -- API keys present
isLoading,
error,
mapStyle, // 'streets' | 'satellite'
} = useMapProvider();

useMapCoordinates

Manages map center and zoom state:

import { useMapCoordinates } from '@/hooks/use-map-coordinates';

const {
center, zoom, bounds,
setCenter, setZoom, setBounds,
fitToBounds,
} = useMapCoordinates(initialCenter, initialZoom);

useGeolocation

Browser geolocation with permission handling:

import { useGeolocation } from '@/hooks/use-geolocation';

const {
coordinates, // Coordinates | null
error, // 'PERMISSION_DENIED' | 'POSITION_UNAVAILABLE' | 'TIMEOUT' | 'NOT_SUPPORTED'
isLoading,
permission, // PermissionState | null
} = useGeolocation();

useLocationItems

Fetches items filtered by geographic proximity:

import { useLocationItems } from '@/hooks/use-location-items';
const { items, isLoading } = useLocationItems(coordinates, radius);

useUserLocation

Manages the user's stored location preference:

import { useUserLocation } from '@/hooks/use-user-location';
const { location, setLocation, clearLocation } = useUserLocation();

Geocoding

The template provides geocoding through the /api/geocode endpoint, supporting both forward geocoding (address to coordinates) and reverse geocoding (coordinates to address). The geocoding service is located at lib/services/geocoding/.

Address Autocomplete

interface AddressSuggestion {
id: string;
mainText: string; // Street address
secondaryText: string; // City, state
fullAddress: string;
coordinates?: Coordinates;
}

Configuration

# Google Maps
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_key
NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID=your_map_id

# Mapbox (alternative)
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=your_token

The provider is automatically selected based on which API key is configured.