Leaflet Store Locator Map
blockdashboardStore locator on free OpenStreetMap tiles — no API key. Pins with bound popups, distance/hours/amenities list, fly-to on select.
Installation
$pnpm dlx uipkge-ng@latest add leaflet-store-locator-map$npx uipkge-ng@latest add leaflet-store-locator-map$yarn dlx uipkge-ng@latest add leaflet-store-locator-map$bunx uipkge-ng@latest add leaflet-store-locator-mapManifest:
https://uipkge.dev/r/angular/leaflet-store-locator-map.jsonInstalls to:src/app/components/blocks/leaflet-store-locator-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.
RetailStoreinterface RetailStore {
id: string
name: string
address: string
neighborhood: string
lat: number
lng: number
distanceMiles: number
hours: string
isOpen: boolean
amenities: string[]
}Includes
Files installed (2)
src/app/components/blocks/leaflet-store-locator-map/leaflet-store-locator-map.component.ts7.1 kB
import { Component, Input, OnInit, ViewChild, signal, ChangeDetectionStrategy } from '@angular/core' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiCardComponent, UiCardContentComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiLeafletMapComponent, UiLeafletMarkerComponent, UiLeafletPopupComponent, } from '@/ui/leaflet-map/leaflet-map.component' export interface RetailStore { id: string name: string address: string neighborhood: string lat: number lng: number distanceMiles: number hours: string isOpen: boolean amenities: string[] } export const DEFAULT_STORES: RetailStore[] = [ { id: 'nyc-herald', name: 'Herald Square', address: '151 W 34th St', neighborhood: 'Midtown', lat: 40.7484, lng: -73.9879, distanceMiles: 0.3, hours: 'Open until 10 PM', isOpen: true, amenities: ['Pickup', 'Returns', 'Cafe'], }, { id: 'nyc-chelsea', name: 'Chelsea Market', address: '75 9th Ave', neighborhood: 'Chelsea', lat: 40.7424, lng: -74.0061, distanceMiles: 0.9, hours: 'Open until 9 PM', isOpen: true, amenities: ['Parking', 'Cafe'], }, { id: 'nyc-soho', name: 'SoHo Flagship', address: '112 Greene St', neighborhood: 'SoHo', lat: 40.7248, lng: -73.9984, distanceMiles: 1.6, hours: 'Open until 9 PM', isOpen: true, amenities: ['Parking', 'Pickup', 'Returns'], }, { id: 'nyc-ues', name: 'Lexington & 78th', address: '1120 Lexington Ave', neighborhood: 'Upper East Side', lat: 40.7736, lng: -73.9614, distanceMiles: 1.8, hours: 'Open until 8 PM', isOpen: true, amenities: ['Pickup', 'Returns'], }, { id: 'nyc-tribeca', name: 'Tribeca', address: '60 Hudson St', neighborhood: 'Tribeca', lat: 40.7175, lng: -74.0089, distanceMiles: 2.1, hours: 'Closed · Opens 10 AM', isOpen: false, amenities: ['Parking', 'Returns'], }, ] // Midtown Manhattan as [lng, lat] — a stable reference so the map's center input never re-fires. const MIDTOWN_CENTER: [number, number] = [-73.985, 40.748] const POPUP_OFFSET: [number, number] = [0, -18] const PIN_SELECTED = 'border-primary bg-primary text-primary-foreground rounded-full border px-2 py-0.5 text-xs font-semibold shadow-xs' const PIN_IDLE = 'border-border bg-card text-foreground rounded-full border px-2 py-0.5 text-xs font-semibold shadow-xs' /** * Store locator on free OpenStreetMap tiles — no API key. Pins with bound * popups, distance/hours/amenities list, fly-to on select (map pin or row). */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-leaflet-store-locator-map, [ui-leaflet-store-locator-map]', standalone: true, imports: [ UiBadgeComponent, UiCardComponent, UiCardHeaderComponent, UiCardTitleComponent, UiCardContentComponent, UiLeafletMapComponent, UiLeafletMarkerComponent, UiLeafletPopupComponent, ], host: { 'data-slot': 'leaflet-store-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">Stores</h2> <p class="text-muted-foreground text-xs">{{ openCount }} open · {{ stores.length }} locations</p> </div> <div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12"> <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"> <ui-leaflet-map variant="streets" [center]="midtownCenter" [zoom]="13" class="absolute inset-0 size-full"> @for (store of stores; track store.id) { <ui-leaflet-marker [lngLat]="[store.lng, store.lat]" anchor="center"> <button type="button" [class]="pinClass(store)" (click)="selectStore(store)">{{ store.name }}</button> <ui-leaflet-popup [offset]="popupOffset" className="space-y-1 text-xs"> <p class="text-foreground font-semibold">{{ store.name }}</p> <p class="text-muted-foreground">{{ store.address }}, {{ store.neighborhood }}</p> <p class="text-muted-foreground">{{ store.hours }} · {{ store.distanceMiles }} mi away</p> </ui-leaflet-popup> </ui-leaflet-marker> } </ui-leaflet-map> </div> </ui-card> <ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4"> <ui-card-header class="border-border border-b p-4"> <ui-card-title class="text-sm font-semibold">Locations</ui-card-title> </ui-card-header> <ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0"> @for (store of stores; track store.id) { <button type="button" [class]="rowClass(store)" (click)="selectStore(store)"> <div class="min-w-0 space-y-1"> <p class="text-foreground truncate text-sm font-medium">{{ store.name }}</p> <p class="text-muted-foreground truncate text-xs"> {{ store.address }}, {{ store.neighborhood }} · {{ store.hours }} </p> <div class="flex flex-wrap gap-1"> @for (amenity of store.amenities; track amenity) { <ui-badge variant="secondary" class="text-xs">{{ amenity }}</ui-badge> } </div> </div> <div class="flex shrink-0 flex-col items-end gap-1"> <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ store.distanceMiles }} mi</span> <ui-badge [variant]="store.isOpen ? 'info' : 'secondary'" class="text-xs"> {{ store.isOpen ? 'Open' : 'Closed' }} </ui-badge> </div> </button> } </ui-card-content> </ui-card> </div> `, }) export class UiLeafletStoreLocatorMapComponent implements OnInit { @Input() stores: RetailStore[] = DEFAULT_STORES @Input('class') className?: string @ViewChild(UiLeafletMapComponent) map?: UiLeafletMapComponent protected readonly midtownCenter = MIDTOWN_CENTER protected readonly popupOffset = POPUP_OFFSET readonly selectedId = signal('') get hostClass(): string { return cn('flex h-full min-h-0 flex-col gap-4', this.className) } get openCount(): number { return this.stores.filter((s) => s.isOpen).length } ngOnInit(): void { this.selectedId.set(this.stores[0]?.id ?? '') } selectStore(store: RetailStore): void { this.selectedId.set(store.id) this.map?.flyTo({ center: [store.lng, store.lat], zoom: 14, duration: 900, }) } pinClass(store: RetailStore): string { return this.selectedId() === store.id ? PIN_SELECTED : PIN_IDLE } rowClass(store: RetailStore): 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() === store.id && 'bg-accent/60', ) } }src/app/components/blocks/leaflet-store-locator-map/index.ts0.1 kB
export { UiLeafletStoreLocatorMapComponent, DEFAULT_STORES, type RetailStore, } from './leaflet-store-locator-map.component'
Raw manifest:https://uipkge.dev/r/angular/leaflet-store-locator-map.json