Render the polygon for any Mapbase place id on a MapLibre map, framed to its bbox. Composes shadcn Card with the mapcn Map primitive.
bunx shadcn@latest add https://mapbase.dev/r/boundary-map.jsonThe shadcn CLI copies the file(s) below into your tree, installs npm dependencies, and recursively pulls registry dependencies (including from cross-registry URLs).
"use client"
import * as React from "react"
import { Mapbase, type Boundary } from "mapbase"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Map,
MapControls,
type MapRef,
type MapViewport,
} from "@/components/ui/map"
import { cn } from "@/lib/utils"
export type BoundaryMapProps = {
/**
* Public Mapbase API key (`mb_live_*`). Mint one at
* https://dashboard.mapbase.dev/api-keys.
*/
apiKey: string
/** Optional: pin the engine URL. Defaults to https://api.mapbase.dev. */
baseUrl?: string
/**
* Place id whose boundary should be loaded. Pass `null` (or omit) to
* leave the map empty until you've resolved one — typically via the
* autocomplete-location or geocode-input components.
*/
placeId?: string | null
/** Optional pre-fetched boundary; skips the network round-trip. */
boundary?: Boundary | null
/** Title rendered in the card header. */
title?: string
className?: string
}
const SOURCE_ID = "mapbase-boundary"
const FILL_LAYER_ID = "mapbase-boundary-fill"
const LINE_LAYER_ID = "mapbase-boundary-line"
const DEFAULT_VIEWPORT: MapViewport = {
center: [-4.5, 40.0],
zoom: 4.5,
bearing: 0,
pitch: 0,
}
/**
* Render the polygon for any Mapbase place id on a MapLibre map.
*
* The map is framed to the boundary bbox automatically; the polygon is
* drawn as a translucent fill plus a stroke. Built on shadcn Card and
* the mapcn `Map` primitive — no other UI deps.
*/
export function BoundaryMap({
apiKey,
baseUrl,
placeId,
boundary: controlledBoundary,
title = "Boundary",
className,
}: BoundaryMapProps) {
const client = React.useMemo(
() => new Mapbase(apiKey, baseUrl ? { baseUrl } : undefined),
[apiKey, baseUrl],
)
const [boundary, setBoundary] = React.useState<Boundary | null>(
controlledBoundary ?? null,
)
const [viewport, setViewport] = React.useState<MapViewport>(DEFAULT_VIEWPORT)
const [errorMessage, setErrorMessage] = React.useState<string | null>(null)
const mapRef = React.useRef<MapRef>(null)
React.useEffect(() => {
if (controlledBoundary !== undefined) {
setBoundary(controlledBoundary)
}
}, [controlledBoundary])
// Fetch when only `placeId` is supplied (not `boundary`).
React.useEffect(() => {
if (controlledBoundary !== undefined) return
if (!placeId) {
setBoundary(null)
return
}
const controller = new AbortController()
void (async () => {
const result = await client.boundaries.byId(placeId, {
signal: controller.signal,
})
if (controller.signal.aborted) return
if (result.error) {
setBoundary(null)
setErrorMessage(humanizeError(result.error.code))
return
}
setBoundary(result.data)
setErrorMessage(null)
})()
return () => controller.abort()
}, [client, placeId, controlledBoundary])
// Frame the map and draw the polygon every time `boundary` changes.
// Sources / layers are added on the underlying MapLibre map directly;
// we tear them down on cleanup so re-renders don't stack duplicates.
React.useEffect(() => {
const map = mapRef.current
if (!map || !boundary) return
const apply = () => {
const existing = map.getSource(SOURCE_ID)
if (existing) {
// mapcn's MapRef exposes the standard MapLibre Map surface.
;(existing as { setData: (geojson: unknown) => void }).setData({
type: "Feature",
properties: {},
geometry: boundary.geometry,
})
} else {
map.addSource(SOURCE_ID, {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: boundary.geometry,
},
})
map.addLayer({
id: FILL_LAYER_ID,
type: "fill",
source: SOURCE_ID,
paint: {
"fill-color": "#3b82f6",
"fill-opacity": 0.18,
},
})
map.addLayer({
id: LINE_LAYER_ID,
type: "line",
source: SOURCE_ID,
paint: {
"line-color": "#2563eb",
"line-width": 2,
},
})
}
map.fitBounds(
[
[boundary.bbox[0], boundary.bbox[1]],
[boundary.bbox[2], boundary.bbox[3]],
],
{ padding: 32, duration: 500 },
)
}
if (map.isStyleLoaded()) apply()
else map.once("load", apply)
return () => {
if (!mapRef.current) return
const m = mapRef.current
if (m.getLayer(LINE_LAYER_ID)) m.removeLayer(LINE_LAYER_ID)
if (m.getLayer(FILL_LAYER_ID)) m.removeLayer(FILL_LAYER_ID)
if (m.getSource(SOURCE_ID)) m.removeSource(SOURCE_ID)
}
}, [boundary])
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>
{boundary
? `bbox: [${boundary.bbox.map((n) => n.toFixed(3)).join(", ")}]`
: "Pass a placeId to load a boundary."}
</CardDescription>
</CardHeader>
<CardContent>
<div className="relative h-[360px] w-full overflow-hidden rounded-md border">
<Map
ref={mapRef}
viewport={viewport}
onViewportChange={setViewport}
className="h-full w-full"
>
<MapControls position="bottom-right" showZoom />
</Map>
{errorMessage ? (
<div className="absolute right-2 top-2 rounded-md bg-background/80 px-2 py-1 text-xs text-destructive backdrop-blur">
{errorMessage}
</div>
) : null}
</div>
</CardContent>
</Card>
)
}
function humanizeError(code: string): string {
switch (code) {
case "not_found":
return "No boundary stored for that place."
case "unauthorized":
return "Invalid API key."
case "rate_limited":
return "Rate limited — try again shortly."
case "network_error":
return "Network error."
default:
return "Failed to load boundary."
}
}