mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-04 07:46:16 +02:00
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>
87 lines
2.4 KiB
TypeScript
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 }
|
|
}
|