All components

Geocode Resolve Input

Debounced coordinate resolver backed by the Mapbase /v1/geocode/resolve endpoint. Watches lng/lat props and surfaces administrative hierarchy.

Live preview

Enter valid coordinates to resolve administrative context.

Install

$bunx shadcn@latest add https://mapbase.dev/r/geocode-resolve-input.json

The shadcn CLI copies the file(s) below into your tree, installs npm dependencies, and recursively pulls registry dependencies (including from cross-registry URLs).

npm dependencies

mapbase

Registry dependencies

None.

Source

components/mapbase/geocode-resolve-input.tsx
"use client"

import * as React from "react"
import {
  Mapbase,
  MapbaseError,
  type CountryCode,
  type GeocodeResolveResponse,
} from "mapbase"

import { cn } from "@/lib/utils"

export type GeocodeResolveInputProps = {
  /**
   * 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
  /** Longitude (WGS84). */
  lng: number
  /** Latitude (WGS84). */
  lat: number
  /** Optional ISO country filter passed to the engine. */
  country?: CountryCode
  /** Fired when a resolve completes (or clears on invalid coords). */
  onChange?: (result: GeocodeResolveResponse | null) => void
  /** Debounce window for coordinate changes. Defaults to 300 ms. */
  debounceMs?: number
  /** Disable outbound requests. */
  disabled?: boolean
  className?: string
}

/**
 * Headless coordinate resolver backed by the Mapbase
 * `/v1/geocode/resolve` endpoint. Watches `lng` / `lat` (and optional
 * `country`), debounces, aborts stale requests, and surfaces the
 * administrative hierarchy inline.
 */
export function GeocodeResolveInput({
  apiKey,
  baseUrl,
  lng,
  lat,
  country,
  onChange,
  debounceMs = 300,
  disabled,
  className,
}: GeocodeResolveInputProps) {
  const [result, setResult] = React.useState<GeocodeResolveResponse | null>(
    null,
  )
  const [loading, setLoading] = React.useState(false)
  const [error, setError] = React.useState<MapbaseError | null>(null)

  const onChangeRef = React.useRef(onChange)
  onChangeRef.current = onChange

  const client = React.useMemo(
    () => new Mapbase(apiKey, baseUrl ? { baseUrl } : undefined),
    [apiKey, baseUrl],
  )

  const countryKey = country ?? ""

  React.useEffect(() => {
    if (disabled) return
    if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
      setResult(null)
      setError(null)
      setLoading(false)
      onChangeRef.current?.(null)
      return
    }

    const controller = new AbortController()
    const timer = window.setTimeout(async () => {
      setLoading(true)
      setError(null)

      const response = await client.geocode.resolve(
        {
          lng,
          lat,
          ...(country ? { country } : {}),
        },
        { signal: controller.signal },
      )

      if (controller.signal.aborted) return

      if (response.error) {
        if (
          response.error.code === "network_error" &&
          controller.signal.aborted
        ) {
          return
        }
        setError(response.error)
        setResult(null)
        onChangeRef.current?.(null)
      } else {
        setResult(response.data)
        onChangeRef.current?.(response.data)
      }
      setLoading(false)
    }, debounceMs)

    return () => {
      controller.abort()
      window.clearTimeout(timer)
    }
  }, [client, lng, lat, countryKey, debounceMs, disabled])

  return (
    <div className={cn("flex flex-col gap-2 text-sm", className)}>
      {loading ? (
        <p className="text-xs text-muted-foreground">Resolving…</p>
      ) : null}

      {error ? (
        <div className="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm">
          <p className="font-medium text-destructive">
            {humanizeErrorCode(error.code)}
          </p>
          <p className="text-muted-foreground">{error.message}</p>
        </div>
      ) : null}

      {!loading && !error && result ? (
        <ResolveSummary result={result} />
      ) : null}

      {!loading && !error && !result ? (
        <p className="text-xs text-muted-foreground">
          Enter valid coordinates to resolve administrative context.
        </p>
      ) : null}
    </div>
  )
}

function ResolveSummary({ result }: { result: GeocodeResolveResponse }) {
  const slots = [
    {
      label: "ad_1",
      name: result.ad_1,
      id: result.ad_1_id,
      uri: result.ad_1_uri,
      kind: result.ad_1_kind,
    },
    {
      label: "ad_2",
      name: result.ad_2,
      id: result.ad_2_id,
      uri: result.ad_2_uri,
      kind: result.ad_2_kind,
    },
    {
      label: "ad_3",
      name: result.ad_3,
      id: result.ad_3_id,
      uri: result.ad_3_uri,
      kind: result.ad_3_kind,
    },
    {
      label: "ad_4",
      name: result.ad_4,
      id: result.ad_4_id,
      uri: result.ad_4_uri,
      kind: result.ad_4_kind,
    },
  ].filter((row) => row.name)

  return (
    <div className="rounded-md border bg-muted/40 p-3">
      <div className="font-medium">
        {result.anchor.name}{" "}
        <span className="text-xs font-normal text-muted-foreground">
          ({result.country})
        </span>
      </div>
      {slots.length > 0 ? (
        <ul className="mt-2 space-y-1 text-xs text-muted-foreground">
          {slots.map((row) => (
            <li key={row.label}>
              <span className="font-mono">{row.label}</span>: {row.name}
              {row.id ? ` · ${row.id}` : ""}
              {row.uri ? ` · ${row.uri}` : ""}
              {row.kind ? ` · ${row.kind}` : ""}
            </li>
          ))}
        </ul>
      ) : null}
      {result.zones.length > 0 ? (
        <p className="mt-2 text-xs text-muted-foreground">
          Zones: {result.zones.join(", ")}
        </p>
      ) : null}
    </div>
  )
}

function humanizeErrorCode(code: MapbaseError["code"]): string {
  switch (code) {
    case "unauthorized":
      return "Invalid API key"
    case "rate_limited":
      return "Rate limited — try again shortly"
    case "invalid_request":
      return "Invalid coordinates"
    case "not_found":
      return "No location at this point"
    case "not_implemented":
      return "Endpoint not yet available"
    case "network_error":
      return "Network error"
    default:
      return "Something went wrong"
  }
}