diff --git a/src/components/DirectionsPanel.jsx b/src/components/DirectionsPanel.jsx
index 44c0f98..a01f1c9 100644
--- a/src/components/DirectionsPanel.jsx
+++ b/src/components/DirectionsPanel.jsx
@@ -1,5 +1,8 @@
-import { useEffect } from "react"
-import { ArrowUpDown, Plus, X, Footprints, Bike, Car, Shield, AlertTriangle, Zap, Trash2, ChevronUp, ChevronDown } from "lucide-react"
+import { useEffect, useMemo } from "react"
+import { ArrowUpDown, Plus, X, Footprints, Bike, Car, Shield, AlertTriangle, Zap, Trash2, GripVertical } from "lucide-react"
+import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"
+import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"
+import { CSS } from "@dnd-kit/utilities"
import { useStore } from "../store"
import LocationInput from "./LocationInput"
import ManeuverList from "./ManeuverList"
@@ -18,6 +21,40 @@ const BOUNDARY_MODES = [
{ id: "emergency", label: "Ignore", Icon: Zap, title: "Ignore barriers" },
]
+// Sortable row component
+function SortableRow({ id, children }) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ zIndex: isDragging ? 1000 : 1,
+ }
+
+ return (
+
+ {/* Drag handle */}
+
+ {children}
+
+ )
+}
+
export default function DirectionsPanel({ onClose }) {
const routeStart = useStore((s) => s.routeStart)
const routeEnd = useStore((s) => s.routeEnd)
@@ -42,6 +79,36 @@ export default function DirectionsPanel({ onClose }) {
const removeStop = useStore((s) => s.removeStop)
const setStops = useStore((s) => s.setStops)
+ // DnD sensors
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ )
+
+ // Build unified list for drag-and-drop: origin + stops + destination
+ // Each item has: { id, type, data }
+ const unifiedList = useMemo(() => {
+ const items = []
+ if (routeStart) {
+ items.push({ id: "origin", type: "origin", data: routeStart })
+ }
+ stops.forEach((stop) => {
+ items.push({ id: stop.id, type: "stop", data: stop })
+ })
+ if (routeEnd) {
+ items.push({ id: "destination", type: "destination", data: routeEnd })
+ }
+ return items
+ }, [routeStart, stops, routeEnd])
+
+ const itemIds = useMemo(() => unifiedList.map((item) => item.id), [unifiedList])
+
// Auto-fill origin with GPS if available and origin is empty
useEffect(() => {
if (!routeStart && geoPermission === "granted" && userLocation) {
@@ -61,13 +128,6 @@ export default function DirectionsPanel({ onClose }) {
}
}, [routeStart?.lat, routeStart?.lon, routeEnd?.lat, routeEnd?.lon])
- const handleSwap = () => {
- const tempStart = routeStart
- const tempEnd = routeEnd
- setRouteStart(tempEnd)
- setRouteEnd(tempStart)
- }
-
const handleClose = () => {
clearRoute()
setDirectionsMode(false)
@@ -78,24 +138,56 @@ export default function DirectionsPanel({ onClose }) {
addIntermediateStop()
}
- const handleMoveStopUp = (idx) => {
- if (idx === 0) return
- const newStops = [...stops]
- const temp = newStops[idx]
- newStops[idx] = newStops[idx - 1]
- newStops[idx - 1] = temp
- setStops(newStops)
- computeRoute()
- }
+ // Handle drag end - reorder the unified list
+ const handleDragEnd = (event) => {
+ const { active, over } = event
+ if (!over || active.id === over.id) return
- const handleMoveStopDown = (idx) => {
- if (idx >= stops.length - 1) return
- const newStops = [...stops]
- const temp = newStops[idx]
- newStops[idx] = newStops[idx + 1]
- newStops[idx + 1] = temp
+ const oldIndex = unifiedList.findIndex((item) => item.id === active.id)
+ const newIndex = unifiedList.findIndex((item) => item.id === over.id)
+
+ if (oldIndex === -1 || newIndex === -1) return
+
+ // Reorder the unified list
+ const reordered = arrayMove(unifiedList, oldIndex, newIndex)
+
+ // Extract new origin, stops, and destination from reordered list
+ // First item becomes origin, last becomes destination, middle are stops
+ if (reordered.length === 0) return
+
+ const newOriginItem = reordered[0]
+ const newDestItem = reordered.length > 1 ? reordered[reordered.length - 1] : null
+ const newStopItems = reordered.length > 2 ? reordered.slice(1, -1) : []
+
+ // Convert items to proper format
+ const newOrigin = newOriginItem.data ? {
+ lat: newOriginItem.data.lat,
+ lon: newOriginItem.data.lon,
+ name: newOriginItem.data.name,
+ source: newOriginItem.data.source,
+ } : null
+
+ const newDest = newDestItem?.data ? {
+ lat: newDestItem.data.lat,
+ lon: newDestItem.data.lon,
+ name: newDestItem.data.name,
+ source: newDestItem.data.source,
+ } : null
+
+ const newStops = newStopItems.map((item) => ({
+ id: item.id === "origin" || item.id === "destination" ? crypto.randomUUID() : item.id,
+ lat: item.data?.lat ?? null,
+ lon: item.data?.lon ?? null,
+ name: item.data?.name ?? "",
+ }))
+
+ // Update state
+ setRouteStart(newOrigin)
+ setRouteEnd(newDest)
setStops(newStops)
- computeRoute()
+
+ // Trigger route recalculation
+ setTimeout(() => computeRoute(), 0)
}
// Check if route has wilderness segments
@@ -117,113 +209,87 @@ export default function DirectionsPanel({ onClose }) {
- {/* Origin/Destination inputs */}
-
- {/* Origin row with swap button on right */}
-
-
-
-
- {/* Swap button - only on origin row, swaps origin and destination */}
-
-
+ {/* Drag-and-drop location list */}
+
+
+
+ {unifiedList.map((item, idx) => (
+
+
+ {item.type === "origin" && (
+
+ )}
+ {item.type === "destination" && (
+
+ )}
+ {item.type === "stop" && (
+ {
+ if (place) {
+ updateStop(item.id, place)
+ }
+ }}
+ placeholder={`Stop ${idx}`}
+ icon="stop"
+ fieldId={`stop-${item.id}`}
+ autoFocus={item.data.lat == null}
+ />
+ )}
+
+ {/* Remove button for intermediate stops only */}
+ {item.type === "stop" && (
+
+ )}
+ {/* Spacer for origin/destination to align with stops that have remove button */}
+ {item.type !== "stop" && (
+
+ )}
+
+ ))}
- {/* Intermediate stops - rendered between origin and destination */}
- {stops.map((stop, idx) => (
-
-
- {
- if (place) {
- updateStop(stop.id, place)
- }
+ {/* Add stop button - only show when route exists */}
+ {routeStart && routeEnd && stops.length < 8 && (
+
-
- {/* Reorder buttons */}
-
-
-
-
- {/* Remove button */}
-
+ )}
- ))}
-
- {/* Destination row */}
-
-
-
-
- {/* Spacer to align with origin row swap button */}
-
-
-
- {/* Add stop button - only show when route exists */}
- {routeStart && routeEnd && stops.length < 8 && (
-
- )}
-
+
+
{/* Travel mode selector */}
diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx
index 0f13fd1..6f6d26b 100644
--- a/src/components/MapView.jsx
+++ b/src/components/MapView.jsx
@@ -1721,22 +1721,20 @@ const MapView = forwardRef(function MapView(_, ref) {
icon: Plus,
onSelect: () => {
setRadialMenu((m) => ({ ...m, open: false }))
- const { stops, addStop } = useStore.getState()
+ const { addIntermediateStop, computeRoute, routeStart, routeEnd } = useStore.getState()
const place = {
lat: radialMenu.lat,
lon: radialMenu.lon,
name: radialMenu.centerLabel || radialMenu.lat.toFixed(5) + ", " + radialMenu.lon.toFixed(5),
- source: "radial_menu",
- matchCode: null,
}
- if (stops.length === 0) {
- addStop(place)
- useStore.setState({ gpsOrigin: false })
- } else {
- const success = addStop(place)
- if (!success) {
- toast("Maximum 10 stops reached")
+ const success = addIntermediateStop(place)
+ if (success) {
+ // If we have both origin and destination, recalculate route
+ if (routeStart && routeEnd) {
+ computeRoute()
}
+ } else {
+ toast("Maximum 8 intermediate stops reached")
}
},
},
diff --git a/src/store.js b/src/store.js
index 474163f..069be9f 100644
--- a/src/store.js
+++ b/src/store.js
@@ -73,14 +73,17 @@ export const useStore = create((set, get) => ({
// ── INTERMEDIATE STOPS MANAGEMENT ──
// stops[] contains ONLY intermediate waypoints, not origin/destination
- addIntermediateStop: () => {
+ // Add intermediate stop - can be called with or without place
+ // With place: creates pre-filled stop (from radial menu)
+ // Without place: creates empty placeholder (from Add Stop button)
+ addIntermediateStop: (place) => {
const { stops } = get()
if (stops.length >= 8) return false // Max 8 intermediate stops
const newStop = {
id: crypto.randomUUID(),
- lat: null,
- lon: null,
- name: "",
+ lat: place?.lat ?? null,
+ lon: place?.lon ?? null,
+ name: place?.name ?? "",
}
set({ stops: [...stops, newStop] })
return true