Skip to content
txbrendan/ui

Payments Globe

beta

A dotted globe you can spin, with payments flying home from their cities as arcs, a rolling total, a top-cities list to pick from, a day you can replay, and a live mode.

Built from Badge, Button, Card, Motion Timing, Notice, Rolling Number, Skeleton, and Slider.

Preview

Arrow keys turn the globe. Home turns it back to San Francisco. Pick a city from the list to turn to it.

Revenue today

$402,784

640 payments

16:42:14 UTCLive
Time of day

loading

Loading payments

empty

Arrow keys turn the globe. Home turns it back to San Francisco. Pick a city from the list to turn to it.

Revenue today

$0

0 payments

Payments show up here as they arrive.

16:42:14 UTC
Time of day

error

Arrow keys turn the globe. Home turns it back to San Francisco. Pick a city from the list to turn to it.

Revenue today

$0

0 payments

16:42:14 UTC
Time of day

narrow container

Arrow keys turn the globe. Home turns it back to San Francisco. Pick a city from the list to turn to it.

Revenue today

$129,842

200 payments

16:42:14 UTC
Time of day

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the block. It also adds every component it uses. Its own files go in your components folder, and they are yours to edit.

    npx shadcn@latest add @txbrendan/payments-globe

Use it for

  • The hero of a revenue or sales dashboard, where seeing money arrive from around the world is the point.
  • Pass one home city, the cities payments come from, and each payment's city, time, and amount. The block works out every total.
  • For live mode, set live and add each new payment to payments as it arrives. The block never fetches anything itself.
  • Pass now from your server when the page must render the same on the server and in the browser, like a demo or a test.
  • Set timeZone to where your day starts. The clock, the hours, and the day all follow it.
  • Pass format as number format options, like { style: "currency", currency: "EUR" }, so a server component can render the block.

Avoid it for

  • Exact places, like store locations or delivery routes. The land is drawn by hand, so it is rough. Use a real map.
  • A plain total, or a total over time. Use Stats Cards or a chart. They are smaller and easier to read.
  • More than about 60 cities. Dots crowd together and the list gets long. Group small places into their nearest big city.

Props

  • home

    The city every payment flies to, like your headquarters. The globe starts facing it.

    { name: string; lat: number; lon: number }

  • cities

    Where payments come from. Ids must be unique. Cities with no payments that day are left off.

    { id: string; name: string; lat: number; lon: number }[]

  • payments

    Each payment: its city's id, its time in ms, and its amount. Any order. Payments outside the day are ignored.

    { cityId: string; at: number; amount: number }[]

  • format

    How amounts print. Options work from a server component. A function works from a client component only.

    Intl.NumberFormatOptions | ((value: number) => string) · default { style: "currency", currency: "USD", maximumFractionDigits: 0 }

  • locale

    The locale for format options.

    string · default "en-US"

  • timeZone

    Where the day starts and how the clock reads, like "America/New_York".

    string · default "UTC"

  • live

    Follows the current time and shows a Live badge. Moving the scrubber leaves it until Back to live.

    boolean · default false

  • now

    The current time in ms. When it is not set, the block reads the clock after it mounts.

    number

  • day

    Any time on the day to show. Default the day of now, or of the newest payment.

    number

  • selectedCityId

    The picked city's id. Leave it out to let the block keep track.

    string | null

  • onSelectedCityChange

    Called with a city's id when someone picks it, or null when they clear it.

    (id: string | null) => void

  • title

    The label over the total. A picked city replaces it with its own name.

    string · default "Revenue today"

  • loading

    Shows placeholders at the same size while the data loads.

    boolean · default false

  • loadingText

    What the status line tells screen readers while loading.

    string · default "Loading payments"

  • error

    Shows this message in place of the payments. The globe stays, with no arcs.

    ReactNode

  • emptyText

    Shown when the day has no payments.

    ReactNode · default "No payments yet today."

  • visibleCities

    How many cities the list shows before Show all.

    number · default 5

  • aria-label

    Names the globe for screen readers. The block adds how many cities and the total so far.

    string · default "Payments by city"

  • className

    Extra classes on the outer div, which holds the container query.

    string

Accessibility

  • The globe is a focusable group named by aria-label, with the number of cities and the total so far. Arrow keys turn it, and Home turns it back to the home city.
  • Turn west and Turn east buttons turn the globe too, so nobody has to drag. A tap on a city dot picks it, but the list is the main way.
  • A touch drag only spins the globe sideways. Dragging up and down still scrolls the page.
  • The city list carries the real numbers. Each city is a button with aria-pressed. Picking one turns the globe to it and shows its own total.
  • At rest the globe sways slowly so home stays in sight. Pause motion stops the sway and the arcs. The motion feel turns them off on its own at 0ms, and the arcs then show still.
  • A status line speaks when someone picks a city, or a replay starts or ends. It never speaks for each payment.
  • The scrubber's name reads the time of day, like "Time of day 16:42:14 UTC". Its value is in minutes.
  • Arcs, dots, and the home city keep system colors in forced-colors mode.

Examples

Today's payments

<PaymentsGlobe
  home={{ name: "San Francisco", lat: 37.77, lon: -122.42 }}
  cities={[
    { id: "nyc", name: "New York", lat: 40.71, lon: -74.01 },
    { id: "lon", name: "London", lat: 51.51, lon: -0.13 },
    { id: "tyo", name: "Tokyo", lat: 35.68, lon: 139.69 },
  ]}
  payments={payments}
/>

Live, in your own time zone

<PaymentsGlobe home={home} cities={cities} payments={payments} live timeZone="America/Los_Angeles" />

In euros

<PaymentsGlobe home={home} cities={cities} payments={payments} format={{ style: "currency", currency: "EUR", maximumFractionDigits: 0 }} locale="de-DE" />

While the payments load

<PaymentsGlobe home={home} cities={[]} payments={[]} loading />