Menu

Change language

GitHub repo

Hub Locator Map

blockdashboard

Global hub locator. Pins on a Mapbox map with a short location list and fly-to on select.

Installation

$npx uipkge-ng@latest add hub-locator-map
Manifest:https://uipkge.dev/r/angular/hub-locator-map.jsonInstalls to:src/app/components/blocks/hub-locator-map/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

HubLocation
interface HubLocation {
  id: string
  code: string
  city: string
  country: string
  lat: number
  lng: number
  latencyMs: number
}

npm dependencies

Includes

Files installed (2)

  • src/app/components/blocks/hub-locator-map/hub-locator-map.component.ts6.6 kB
    // Global hub locator. Pins on a Mapbox map with a short location list beside it;
    // selecting a hub (pin or row) flies the camera there.
    import {
      type AfterViewInit,
      Component,
      type ElementRef,
      Input,
      type OnDestroy,
      type OnInit,
      type QueryList,
      ViewChildren,
      signal,
      ChangeDetectionStrategy,
    } from '@angular/core'
    import type * as mapboxgl from 'mapbox-gl'
    import { cn } from '@/lib/utils'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiMapComponent } from '@/ui/map/map.component'
    
    export interface HubLocation {
      id: string
      code: string
      city: string
      country: string
      lat: number
      lng: number
      latencyMs: number
    }
    
    export const DEFAULT_HUBS: HubLocation[] = [
      {
        id: 'iad-01',
        code: 'IAD-01',
        city: 'Ashburn',
        country: 'United States',
        lat: 39.0438,
        lng: -77.4874,
        latencyMs: 11,
      },
      {
        id: 'lhr-01',
        code: 'LHR-01',
        city: 'London',
        country: 'United Kingdom',
        lat: 51.5074,
        lng: -0.1278,
        latencyMs: 14,
      },
      { id: 'fra-01', code: 'FRA-01', city: 'Frankfurt', country: 'Germany', lat: 50.1109, lng: 8.6821, latencyMs: 13 },
      { id: 'nrt-01', code: 'NRT-01', city: 'Tokyo', country: 'Japan', lat: 35.6762, lng: 139.6503, latencyMs: 19 },
      { id: 'sin-01', code: 'SIN-01', city: 'Singapore', country: 'Singapore', lat: 1.3521, lng: 103.8198, latencyMs: 22 },
      { id: 'gru-01', code: 'GRU-01', city: 'São Paulo', country: 'Brazil', lat: -23.5505, lng: -46.6333, latencyMs: 42 },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-hub-locator-map, [ui-hub-locator-map]',
      standalone: true,
      imports: [UiCardComponent, UiCardHeaderComponent, UiCardTitleComponent, UiCardContentComponent, UiMapComponent],
      host: {
        'data-slot': 'hub-locator-map',
        '[class]': 'hostClass',
      },
      template: `
        <div class="flex flex-wrap items-center justify-between gap-3">
          <h2 class="text-foreground text-lg font-semibold tracking-tight">Hubs</h2>
          <p class="text-muted-foreground text-xs">{{ hubs.length }} locations</p>
        </div>
    
        <div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12">
          <div ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-8">
            <div class="relative min-h-[360px] flex-1 overflow-hidden">
              <div
                ui-map
                [accessToken]="accessToken ?? ''"
                variant="dark"
                [center]="[10, 25]"
                [zoom]="1.6"
                class="absolute inset-0 size-full"
                (created)="onMapCreated($event)"
              >
                <!-- Marker elements: handed to mapbox-gl Markers once the map exists, which moves the
                     inner #marker node onto the map; the outer node stays put for Angular's @for. -->
                <div class="hidden">
                  @for (hub of hubs; track hub.id) {
                    <div>
                      <div #marker>
                        <button type="button" [class]="markerClass(hub.id)" (click)="selectHub(hub)">
                          {{ hub.code }}
                        </button>
                      </div>
                    </div>
                  }
                </div>
              </div>
            </div>
          </div>
    
          <div ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4">
            <div ui-card-header class="border-border border-b p-4">
              <h3 ui-card-title class="text-sm font-semibold">Locations</h3>
            </div>
            <div ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0">
              @for (hub of hubs; track hub.id) {
                <button type="button" [class]="rowClass(hub.id)" (click)="selectHub(hub)">
                  <div class="min-w-0">
                    <p class="text-foreground truncate text-sm font-medium">{{ hub.code }}</p>
                    <p class="text-muted-foreground truncate text-xs">{{ hub.city }}, {{ hub.country }}</p>
                  </div>
                  <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ hub.latencyMs }}ms</span>
                </button>
              }
            </div>
          </div>
        </div>
      `,
    })
    export class UiHubLocatorMapComponent implements OnInit, AfterViewInit, OnDestroy {
      @Input() hubs: HubLocation[] = DEFAULT_HUBS
      @Input() accessToken?: string
      @Input('class') className?: string
    
      @ViewChildren('marker') private markerEls?: QueryList<ElementRef<HTMLDivElement>>
    
      readonly selectedId = signal('')
    
      private map: mapboxgl.Map | null = null
      private markers: mapboxgl.Marker[] = []
      private destroyed = false
    
      get hostClass(): string {
        return cn('flex h-full min-h-0 flex-col gap-4', this.className)
      }
    
      ngOnInit(): void {
        this.selectedId.set(this.hubs[0]?.id ?? '')
      }
    
      ngAfterViewInit(): void {
        // A new hubs list re-renders the marker elements; put the new set on the map.
        this.markerEls?.changes.subscribe(() => void this.syncMarkers())
      }
    
      ngOnDestroy(): void {
        this.destroyed = true
        this.clearMarkers()
        this.map = null
      }
    
      markerClass(id: string): string {
        return this.selectedId() === id
          ? 'border-primary bg-primary text-primary-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'
          : 'border-border bg-card text-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'
      }
    
      rowClass(id: string): string {
        return cn(
          'hover:bg-accent/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors',
          this.selectedId() === id && 'bg-accent/60',
        )
      }
    
      onMapCreated(map: mapboxgl.Map): void {
        this.map = map
        void this.syncMarkers()
      }
    
      selectHub(hub: HubLocation): void {
        this.selectedId.set(hub.id)
        this.map?.flyTo?.({
          center: [hub.lng, hub.lat],
          zoom: 5.5,
          essential: true,
          duration: 900,
        })
      }
    
      /** One mapbox-gl Marker per hub, anchored at its centre (the React MapMarker, anchor="center"). */
      private async syncMarkers(): Promise<void> {
        const map = this.map
        const els = this.markerEls?.toArray() ?? []
        if (!map) return
        const mod = await import('mapbox-gl')
        const mapbox = mod.default ?? mod
        if (this.destroyed || map !== this.map) return
        this.clearMarkers()
        this.markers = els.flatMap((el, i) => {
          const hub = this.hubs[i]
          if (!hub) return []
          return [
            new mapbox.Marker({ element: el.nativeElement, anchor: 'center' }).setLngLat([hub.lng, hub.lat]).addTo(map),
          ]
        })
      }
    
      private clearMarkers(): void {
        for (const m of this.markers) m.remove()
        this.markers = []
      }
    }
    
  • src/app/components/blocks/hub-locator-map/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/hub-locator-map.json