feat(maps): add notes input to map pin placement popup (#926)
The map_markers backend has accepted a `notes` column since PR #770 and
the popup display path was wired up to render it (commit 6328256), but
the placement UI never got an input. Result: notes are stored,
displayed when present, and impossible to actually enter via the UI.
Add a notes textarea below the name input in the placement popup,
thread the value through `addMarker` and `createMapMarker`, and trim +
null-coalesce on save. Notes display in the marker popup on click is
unchanged and now actually reachable.
- admin/inertia/lib/api.ts: extend createMapMarker request type with
optional notes
- admin/inertia/hooks/useMapMarkers.ts: addMarker accepts and forwards
notes (response already populated notes into local state, so no
display-side change needed)
- admin/inertia/components/maps/MapComponent.tsx: markerNotes state,
textarea after name input, threaded into handleSaveMarker
Edit-mode for existing markers (so users can backfill notes on
already-placed pins) is intentionally out of scope here - selected-marker
popup is still read-only. That's a follow-up PR if there's demand.
This commit is contained in:
parent
6a4f02dd46
commit
59c2fdbb32
|
|
@ -72,6 +72,7 @@ export default function MapComponent({
|
||||||
const [isDraggingMap, setIsDraggingMap] = useState(false)
|
const [isDraggingMap, setIsDraggingMap] = useState(false)
|
||||||
const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null)
|
const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null)
|
||||||
const [markerName, setMarkerName] = useState('')
|
const [markerName, setMarkerName] = useState('')
|
||||||
|
const [markerNotes, setMarkerNotes] = useState('')
|
||||||
const [markerColor, setMarkerColor] = useState<PinColorId>('orange')
|
const [markerColor, setMarkerColor] = useState<PinColorId>('orange')
|
||||||
const [selectedMarkerId, setSelectedMarkerId] = useState<number | null>(null)
|
const [selectedMarkerId, setSelectedMarkerId] = useState<number | null>(null)
|
||||||
|
|
||||||
|
|
@ -153,18 +154,27 @@ export default function MapComponent({
|
||||||
const handleMapClick = useCallback((e: MapLayerMouseEvent) => {
|
const handleMapClick = useCallback((e: MapLayerMouseEvent) => {
|
||||||
setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat })
|
setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat })
|
||||||
setMarkerName('')
|
setMarkerName('')
|
||||||
|
setMarkerNotes('')
|
||||||
setMarkerColor('orange')
|
setMarkerColor('orange')
|
||||||
setSelectedMarkerId(null)
|
setSelectedMarkerId(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleSaveMarker = useCallback(() => {
|
const handleSaveMarker = useCallback(() => {
|
||||||
if (placingMarker && markerName.trim()) {
|
if (placingMarker && markerName.trim()) {
|
||||||
addMarker(markerName.trim(), placingMarker.lng, placingMarker.lat, markerColor)
|
const trimmedNotes = markerNotes.trim()
|
||||||
|
addMarker(
|
||||||
|
markerName.trim(),
|
||||||
|
placingMarker.lng,
|
||||||
|
placingMarker.lat,
|
||||||
|
markerColor,
|
||||||
|
trimmedNotes ? trimmedNotes : null
|
||||||
|
)
|
||||||
setPlacingMarker(null)
|
setPlacingMarker(null)
|
||||||
setMarkerName('')
|
setMarkerName('')
|
||||||
|
setMarkerNotes('')
|
||||||
setMarkerColor('orange')
|
setMarkerColor('orange')
|
||||||
}
|
}
|
||||||
}, [placingMarker, markerName, markerColor, addMarker])
|
}, [placingMarker, markerName, markerNotes, markerColor, addMarker])
|
||||||
|
|
||||||
const handleFlyTo = useCallback((longitude: number, latitude: number) => {
|
const handleFlyTo = useCallback((longitude: number, latitude: number) => {
|
||||||
mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 })
|
mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 })
|
||||||
|
|
@ -317,6 +327,17 @@ export default function MapComponent({
|
||||||
className="block w-full rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500"
|
className="block w-full rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
placeholder="Notes (optional)"
|
||||||
|
value={markerNotes}
|
||||||
|
onChange={(e) => setMarkerNotes(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') setPlacingMarker(null)
|
||||||
|
}}
|
||||||
|
rows={2}
|
||||||
|
className="mt-1.5 block w-full resize-y rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mt-1.5 flex gap-1 items-center">
|
<div className="mt-1.5 flex gap-1 items-center">
|
||||||
{PIN_COLORS.map((c) => (
|
{PIN_COLORS.map((c) => (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,14 @@ export function useMapMarkers() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addMarker = useCallback(
|
const addMarker = useCallback(
|
||||||
async (name: string, longitude: number, latitude: number, color: PinColorId = 'orange') => {
|
async (
|
||||||
const result = await api.createMapMarker({ name, longitude, latitude, color })
|
name: string,
|
||||||
|
longitude: number,
|
||||||
|
latitude: number,
|
||||||
|
color: PinColorId = 'orange',
|
||||||
|
notes: string | null = null
|
||||||
|
) => {
|
||||||
|
const result = await api.createMapMarker({ name, longitude, latitude, color, notes })
|
||||||
if (result) {
|
if (result) {
|
||||||
const marker: MapMarker = {
|
const marker: MapMarker = {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
|
|
|
||||||
|
|
@ -720,7 +720,7 @@ class API {
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMapMarker(data: { name: string; longitude: number; latitude: number; color?: string }) {
|
async createMapMarker(data: { name: string; longitude: number; latitude: number; color?: string; notes?: string | null }) {
|
||||||
return catchInternal(async () => {
|
return catchInternal(async () => {
|
||||||
const response = await this.client.post<
|
const response = await this.client.post<
|
||||||
{ id: number; name: string; longitude: number; latitude: number; color: string; notes: string | null; created_at: string }
|
{ id: number; name: string; longitude: number; latitude: number; color: string; notes: string | null; created_at: string }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue