Menu

Change language

GitHub repo

Delivery Tracking Map

blocklogistics

Live order delivery route showing courier location, dropoff destination, and real-time ETA countdown.

Installation

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

Variants

Loading interactive previews…

npm dependencies

Files installed (2)

  • src/app/components/blocks/delivery-tracking-map/delivery-tracking-map.component.ts9.3 kB
    // Live order delivery route on a Mapbox map: courier location, dropoff destination and
    // the merchant origin with a real-time ETA countdown side panel. Port of the React block
    // 1:1; markers follow the last-mile-courier-dispatch Angular pattern.
    import {
      type AfterViewInit,
      ChangeDetectionStrategy,
      Component,
      type ElementRef,
      Input,
      type OnDestroy,
      type QueryList,
      ViewChildren,
    } from '@angular/core'
    import type * as mapboxgl from 'mapbox-gl'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiMapComponent, type MapLayerInput, type MapSourceInput } from '@/ui/map/map.component'
    
    export const DELIVERY_ORIGIN: [number, number] = [-122.4194, 37.7749]
    export const DELIVERY_DESTINATION: [number, number] = [-122.392, 37.7915]
    export const COURIER_POSITION: [number, number] = [-122.405, 37.783]
    
    export const DELIVERY_ROUTE_COORDS: [number, number][] = [
      [-122.4194, 37.7749],
      [-122.414, 37.778],
      [-122.405, 37.783],
      [-122.399, 37.787],
      [-122.392, 37.7915],
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-delivery-tracking-map, [ui-delivery-tracking-map]',
      standalone: true,
      host: {
        '[attr.data-slot]': '"delivery-tracking-map"',
        '[class]': 'hostClass',
      },
      imports: [
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiMapComponent,
      ],
      template: `
        <div class="grid gap-4 lg:grid-cols-[1fr_320px]">
          <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border">
            <div
              ui-map
              variant="light"
              [accessToken]="accessToken ?? ''"
              [center]="courierPosition"
              [zoom]="13.5"
              class="h-full w-full"
              [sources]="routeSources"
              [layers]="routeLayers"
              (created)="onMapCreated($event)"
            >
              <!-- Marker elements: handed to mapbox-gl Markers once the map exists, which moves each
                   inner #marker node onto the map; the outer node stays put for Angular's @for. -->
              <div class="hidden">
                <div>
                  <!-- Restaurant / Merchant -->
                  <div #marker>
                    <div class="flex flex-col items-center">
                      <span class="bg-muted-foreground ring-muted size-3 rounded-full ring-4"></span>
                      <span class="border-border bg-background/90 mt-1 rounded border px-1.5 py-0.5 font-mono text-xs"
                        >Merchant</span
                      >
                    </div>
                  </div>
                </div>
                <div>
                  <!-- Live Courier -->
                  <div #marker>
                    <div class="relative flex cursor-pointer items-center justify-center" (click)="focusCourier()">
                      <span class="bg-primary/25 absolute size-7 animate-ping rounded-full"></span>
                      <div
                        class="bg-primary text-primary-foreground ring-background flex size-7 items-center justify-center rounded-full shadow-md ring-2"
                      >
                        🛵
                      </div>
                    </div>
                  </div>
                </div>
                <div>
                  <!-- Destination -->
                  <div #marker>
                    <div class="flex cursor-pointer flex-col items-center" (click)="focusDestination()">
                      <span class="bg-success ring-success/20 size-3 rounded-full ring-4"></span>
                      <span
                        class="border-border bg-background/90 text-success mt-1 rounded border px-1.5 py-0.5 font-mono text-xs font-semibold"
                      >
                        Delivery Dropoff
                      </span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Telemetry & Order Status -->
          <div ui-card class="flex flex-col justify-between">
            <div ui-card-header>
              <div class="flex items-center justify-between">
                <span ui-badge variant="outline" class="font-mono text-xs">ORDER #8924</span>
                <span ui-badge class="border-success/20 bg-success/10 text-success">Approaching</span>
              </div>
              <h3 ui-card-title class="mt-2 text-lg">Express Courier</h3>
              <p ui-card-description>Estimated delivery in <strong class="text-foreground">8 mins</strong> (0.8 mi away)</p>
            </div>
            <div ui-card-content class="space-y-4">
              <div class="border-border space-y-2 border-t pt-3">
                <div class="flex items-center justify-between text-xs">
                  <span class="text-muted-foreground">Driver</span>
                  <span class="text-foreground font-medium">Carlos M. (★ 4.98)</span>
                </div>
                <div class="flex items-center justify-between text-xs">
                  <span class="text-muted-foreground">Vehicle</span>
                  <span class="text-foreground font-mono">E-Scooter #412</span>
                </div>
                <div class="flex items-center justify-between text-xs">
                  <span class="text-muted-foreground">Destination</span>
                  <span class="text-foreground max-w-[170px] truncate font-medium">Market St & 2nd Ave</span>
                </div>
              </div>
    
              <div class="border-border space-y-1.5 border-t pt-3 font-mono text-xs">
                <div class="text-muted-foreground flex items-center gap-2">
                  <span class="bg-success size-1.5 rounded-full"></span>
                  <span>12:42 PM — Order picked up</span>
                </div>
                <div class="text-foreground flex items-center gap-2 font-medium">
                  <span class="bg-primary size-1.5 rounded-full"></span>
                  <span>12:51 PM — Approaching building</span>
                </div>
              </div>
    
              <div class="flex gap-2 pt-2">
                <button ui-button variant="outline" size="sm" class="flex-1" (click)="focusCourier()">Track Courier</button>
                <button ui-button variant="outline" size="sm" class="flex-1" (click)="focusDestination()">
                  Dropoff Point
                </button>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiDeliveryTrackingMapComponent implements AfterViewInit, OnDestroy {
      @Input() accessToken?: string
      @Input('class') className?: string
    
      @ViewChildren('marker') private markerEls?: QueryList<ElementRef<HTMLDivElement>>
    
      readonly origin: [number, number] = [...DELIVERY_ORIGIN] as [number, number]
      readonly destination: [number, number] = [...DELIVERY_DESTINATION] as [number, number]
      readonly courierPosition: [number, number] = [...COURIER_POSITION] as [number, number]
    
      /** The delivery route polyline (the React block's MapSource). */
      readonly routeSources: MapSourceInput[] = [
        {
          id: 'delivery-route',
          type: 'geojson',
          data: {
            type: 'Feature',
            properties: {},
            geometry: { type: 'LineString', coordinates: DELIVERY_ROUTE_COORDS.map((c) => [...c]) },
          },
        },
      ]
    
      /** The blue route line (the React block's MapLayer). */
      readonly routeLayers: MapLayerInput[] = [
        {
          id: 'delivery-route-line',
          type: 'line',
          source: 'delivery-route',
          layout: { 'line-cap': 'round', 'line-join': 'round' },
          paint: { 'line-color': '#2563eb', 'line-width': 4 },
        },
      ]
    
      private map: mapboxgl.Map | null = null
      private markers: mapboxgl.Marker[] = []
      private destroyed = false
    
      get hostClass(): string {
        return cn('block w-full', this.className)
      }
    
      ngAfterViewInit(): void {
        // A new marker 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
      }
    
      focusCourier(): void {
        this.map?.flyTo?.({ center: this.courierPosition, zoom: 14.5 })
      }
    
      focusDestination(): void {
        this.map?.flyTo?.({ center: this.destination, zoom: 15 })
      }
    
      onMapCreated(map: mapboxgl.Map): void {
        this.map = map
        void this.syncMarkers()
      }
    
      /** One mapbox-gl Marker per waypoint: merchant and dropoff anchored at the bottom
          (the React MapMarkers), the courier centred (React 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()
        const waypoints: { coords: [number, number]; anchor: 'bottom' | 'center' }[] = [
          { coords: this.origin, anchor: 'bottom' },
          { coords: this.courierPosition, anchor: 'center' },
          { coords: this.destination, anchor: 'bottom' },
        ]
        this.markers = els.flatMap((el, i) => {
          const waypoint = waypoints[i]
          if (!waypoint) return []
          return [
            new mapbox.Marker({ element: el.nativeElement, anchor: waypoint.anchor }).setLngLat(waypoint.coords).addTo(map),
          ]
        })
      }
    
      private clearMarkers(): void {
        for (const m of this.markers) m.remove()
        this.markers = []
      }
    }
    
  • src/app/components/blocks/delivery-tracking-map/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/delivery-tracking-map.json