Fleet Vehicle Map
blocklogisticsLive fleet map with vehicle markers. Select a unit to fly the camera and read status.
Installation
$pnpm dlx uipkge-ng@latest add fleet-vehicle-map$npx uipkge-ng@latest add fleet-vehicle-map$yarn dlx uipkge-ng@latest add fleet-vehicle-map$bunx uipkge-ng@latest add fleet-vehicle-mapManifest:
https://uipkge.dev/r/angular/fleet-vehicle-map.jsonInstalls to:src/app/components/blocks/fleet-vehicle-map/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
FleetVehicleinterface FleetVehicle {
id: string
identifier: string
model: string
driver: string
status: VehicleStatus
speed: number
lat: number
lng: number
}Files installed (2)
src/app/components/blocks/fleet-vehicle-map/fleet-vehicle-map.component.ts7.6 kB
// Live fleet map with vehicle markers. Select a unit to fly the camera and read status. // Port of the React block 1:1 on Mapbox; markers follow the hub-locator-map Angular pattern. import { type AfterViewInit, ChangeDetectionStrategy, Component, type ElementRef, Input, type OnDestroy, type OnInit, type QueryList, ViewChildren, signal, } from '@angular/core' import type * as mapboxgl from 'mapbox-gl' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiCardComponent, UiCardContentComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiMapComponent } from '@/ui/map/map.component' export type VehicleStatus = 'moving' | 'idling' | 'parked' export interface FleetVehicle { id: string identifier: string model: string driver: string status: VehicleStatus speed: number lat: number lng: number } export const DEFAULT_FLEET_VEHICLES: FleetVehicle[] = [ { id: 'v-104', identifier: 'Truck #104', model: 'Volvo VNL 860', driver: 'Marcus Vance', status: 'moving', speed: 64, lat: 41.5868, lng: -93.625, }, { id: 'v-208', identifier: 'Van #208', model: 'Ford E-Transit 350', driver: 'Elena Rostova', status: 'moving', speed: 48, lat: 41.6005, lng: -93.712, }, { id: 'v-109', identifier: 'Truck #109', model: 'Freightliner Cascadia', driver: 'Derrick Hayes', status: 'idling', speed: 0, lat: 41.984, lng: -93.582, }, { id: 'v-212', identifier: 'Van #212', model: 'Mercedes Sprinter', driver: 'Aaliyah Patel', status: 'moving', speed: 52, lat: 41.591, lng: -93.604, }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-fleet-vehicle-map, [ui-fleet-vehicle-map]', standalone: true, imports: [ UiBadgeComponent, UiCardComponent, UiCardContentComponent, UiCardHeaderComponent, UiCardTitleComponent, UiMapComponent, ], host: { 'data-slot': 'fleet-vehicle-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">Fleet</h2> <p class="text-muted-foreground text-xs">{{ vehicles.length }} units</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]="mapCenter()" [zoom]="11" 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 (vehicle of vehicles; track vehicle.id) { <div> <div #marker> <button type="button" [class]="markerClass(vehicle.id)" (click)="selectVehicle(vehicle)"> {{ markerLabel(vehicle.identifier) }} </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">Vehicles</h3> </div> <div ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0"> @for (vehicle of vehicles; track vehicle.id) { <button type="button" [class]="rowClass(vehicle.id)" (click)="selectVehicle(vehicle)"> <div class="min-w-0"> <p class="text-foreground truncate text-sm font-medium">{{ vehicle.identifier }}</p> <p class="text-muted-foreground truncate text-xs">{{ vehicle.driver }} · {{ vehicle.model }}</p> </div> <span ui-badge [variant]="statusVariant(vehicle.status)" class="shrink-0 text-xs capitalize"> {{ vehicle.status === 'moving' ? vehicle.speed + ' mph' : vehicle.status }} </span> </button> } </div> </div> </div> `, }) export class UiFleetVehicleMapComponent implements OnInit, AfterViewInit, OnDestroy { @Input() vehicles: FleetVehicle[] = DEFAULT_FLEET_VEHICLES.map((v) => ({ ...v })) @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.vehicles[0]?.id ?? '') } ngAfterViewInit(): void { // A new vehicles 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 } selected(): FleetVehicle | undefined { return this.vehicles.find((v) => v.id === this.selectedId()) ?? this.vehicles[0] } mapCenter(): [number, number] { const s = this.selected() return s ? [s.lng, s.lat] : [-93.625, 41.59] } markerLabel(identifier: string): string { return identifier.replace('Truck ', '').replace('Van ', '') } statusVariant(status: VehicleStatus): 'info' | 'warning' | 'secondary' { if (status === 'moving') return 'info' if (status === 'idling') return 'warning' return 'secondary' } 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() } selectVehicle(vehicle: FleetVehicle): void { this.selectedId.set(vehicle.id) this.map?.flyTo?.({ center: [vehicle.lng, vehicle.lat], zoom: 12, essential: true, duration: 800, }) } /** One mapbox-gl Marker per vehicle, 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 vehicle = this.vehicles[i] if (!vehicle) return [] return [ new mapbox.Marker({ element: el.nativeElement, anchor: 'center' }) .setLngLat([vehicle.lng, vehicle.lat]) .addTo(map), ] }) } private clearMarkers(): void { for (const m of this.markers) m.remove() this.markers = [] } }src/app/components/blocks/fleet-vehicle-map/index.ts0.1 kB
export { UiFleetVehicleMapComponent, DEFAULT_FLEET_VEHICLES, type FleetVehicle, type VehicleStatus, } from './fleet-vehicle-map.component'
Raw manifest:https://uipkge.dev/r/angular/fleet-vehicle-map.json