project-nomad/admin/inertia/hooks/useMapMarkers.ts
chriscrosstalk 0183b42d71 feat(maps): add scale bar and location markers (#636)
Add distance scale bar and user-placed location pins to the offline maps viewer.

- Scale bar (bottom-left) shows distance reference that updates with zoom level
- Click anywhere on map to place a named pin with color selection (6 colors)
- Collapsible "Saved Locations" panel lists all pins with fly-to navigation
- Full dark mode support for popups and panel via CSS overrides
- New `map_markers` table with future-proofed columns for routing (marker_type,
  route_id, route_order, notes) to avoid a migration when routes are added later
- CRUD endpoints: GET/POST /api/maps/markers, PATCH/DELETE /api/maps/markers/:id
- VineJS validation on create/update
- MapMarker Lucid model

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:26:50 -07:00

87 lines
2.4 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react'
import api from '~/lib/api'
export const PIN_COLORS = [
{ id: 'orange', label: 'Orange', hex: '#a84a12' },
{ id: 'red', label: 'Red', hex: '#994444' },
{ id: 'green', label: 'Green', hex: '#424420' },
{ id: 'blue', label: 'Blue', hex: '#2563eb' },
{ id: 'purple', label: 'Purple', hex: '#7c3aed' },
{ id: 'yellow', label: 'Yellow', hex: '#ca8a04' },
] as const
export type PinColorId = typeof PIN_COLORS[number]['id']
export interface MapMarker {
id: number
name: string
longitude: number
latitude: number
color: PinColorId
createdAt: string
}
export function useMapMarkers() {
const [markers, setMarkers] = useState<MapMarker[]>([])
const [loaded, setLoaded] = useState(false)
// Load markers from API on mount
useEffect(() => {
api.listMapMarkers().then((data) => {
if (data) {
setMarkers(
data.map((m) => ({
id: m.id,
name: m.name,
longitude: m.longitude,
latitude: m.latitude,
color: m.color as PinColorId,
createdAt: m.created_at,
}))
)
}
setLoaded(true)
})
}, [])
const addMarker = useCallback(
async (name: string, longitude: number, latitude: number, color: PinColorId = 'orange') => {
const result = await api.createMapMarker({ name, longitude, latitude, color })
if (result) {
const marker: MapMarker = {
id: result.id,
name: result.name,
longitude: result.longitude,
latitude: result.latitude,
color: result.color as PinColorId,
createdAt: result.created_at,
}
setMarkers((prev) => [...prev, marker])
return marker
}
return null
},
[]
)
const updateMarker = useCallback(async (id: number, updates: { name?: string; color?: string }) => {
const result = await api.updateMapMarker(id, updates)
if (result) {
setMarkers((prev) =>
prev.map((m) =>
m.id === id
? { ...m, name: result.name, color: result.color as PinColorId }
: m
)
)
}
}, [])
const deleteMarker = useCallback(async (id: number) => {
await api.deleteMapMarker(id)
setMarkers((prev) => prev.filter((m) => m.id !== id))
}, [])
return { markers, loaded, addMarker, updateMarker, deleteMarker }
}