ui

Address Input

Combined address entry primitive.

Installation

pnpm dlx shadcn@latest add https://registry.circle.health/r/address-input.json

Registry dependencies

Component

"use client";

import * as React from "react";

import { Dropdown, TextBox } from "@/registry/berlin/ui/form";

export type SupportedLocale = "en" | "de";

export interface Address {
  address?: string;
  address2?: string;
  city?: string;
  postal_code?: string;
  country?: string;
  country_code?: string;
  province?: string;
}

const labels = {
  en: {
    search: "Search address",
    address: "Street address",
    address2: "Address line 2",
    city: "City",
    postal_code: "Postal code",
    country: "Country",
    province: "State / Province"
  },
  de: {
    search: "Adresse suchen",
    address: "Straße und Hausnummer",
    address2: "Adresszusatz",
    city: "Stadt",
    postal_code: "Postleitzahl",
    country: "Land",
    province: "Bundesland"
  }
} satisfies Record<SupportedLocale, Record<string, string>>;

function updateAddress(
  value: Address | undefined,
  onChange: ((address: Address) => void) | undefined,
  key: keyof Address,
  nextValue: string
) {
  onChange?.({
    ...(value ?? {}),
    [key]: nextValue
  });
}

export interface AutoAddressInputsProps {
  locale?: SupportedLocale;
  accessToken?: string;
  onSelectAddress?: (address: Address) => void;
}

export function AutoAddressInputs({
  locale = "en",
  onSelectAddress
}: AutoAddressInputsProps) {
  return (
    <TextBox
      name="address-search"
      label={labels[locale].search}
      placeholder={labels[locale].address}
      onChange={(event) =>
        onSelectAddress?.({
          address: event.target.value
        })
      }
    />
  );
}

export interface ManualAddressInputsProps {
  value?: Address;
  locale?: SupportedLocale;
  onChange?: (address: Address) => void;
}

export function ManualAddressInputs({
  value,
  locale = "en",
  onChange
}: ManualAddressInputsProps) {
  return (
    <div className="grid gap-4">
      <TextBox
        name="address"
        label={labels[locale].address}
        value={value?.address ?? ""}
        onChange={(event) => updateAddress(value, onChange, "address", event.target.value)}
      />
      <TextBox
        name="address2"
        label={labels[locale].address2}
        value={value?.address2 ?? ""}
        onChange={(event) => updateAddress(value, onChange, "address2", event.target.value)}
      />
      <div className="grid gap-4 sm:grid-cols-2">
        <TextBox
          name="city"
          label={labels[locale].city}
          value={value?.city ?? ""}
          onChange={(event) => updateAddress(value, onChange, "city", event.target.value)}
        />
        <TextBox
          name="postal_code"
          label={labels[locale].postal_code}
          value={value?.postal_code ?? ""}
          onChange={(event) => updateAddress(value, onChange, "postal_code", event.target.value)}
        />
      </div>
      <div className="grid gap-4 sm:grid-cols-2">
        <TextBox
          name="province"
          label={labels[locale].province}
          value={value?.province ?? ""}
          onChange={(event) => updateAddress(value, onChange, "province", event.target.value)}
        />
        <Dropdown
          name="country"
          label={labels[locale].country}
          value={value?.country_code ?? ""}
          onChange={(event) => {
            const next = event.target.value;
            onChange?.({
              ...(value ?? {}),
              country_code: next,
              country: next === "DE" ? "Germany" : next === "US" ? "United States" : next
            });
          }}
          options={{
            DE: locale === "de" ? "Deutschland" : "Germany",
            US: locale === "de" ? "Vereinigte Staaten" : "United States",
            GB: locale === "de" ? "Vereinigtes Königreich" : "United Kingdom"
          }}
        />
      </div>
    </div>
  );
}

export interface AddressInputProps extends ManualAddressInputsProps {
  accessToken?: string;
}

export function AddressInput({
  value,
  onChange,
  locale = "en"
}: AddressInputProps) {
  return (
    <div className="grid gap-4">
      <AutoAddressInputs locale={locale} onSelectAddress={(address) => onChange?.({ ...(value ?? {}), ...address })} />
      <ManualAddressInputs locale={locale} value={value} onChange={onChange} />
    </div>
  );
}
Raw registry JSON/r/address-input.json