diff --git a/assets/js/location_map_hook.ts b/assets/js/location_map_hook.ts
index 0195f786..bc68017c 100644
--- a/assets/js/location_map_hook.ts
+++ b/assets/js/location_map_hook.ts
@@ -3,12 +3,31 @@ import { updateGridOverlay } from "./maidenhead_grid"
interface LocationMapHook extends ViewHook {
map: L.Map
- marker: L.CircleMarker
+ marker: L.Marker
gridLayer: L.LayerGroup
gridVisible: boolean
visibilityHandler: (() => void) | null
}
+// Same red dot the old CircleMarker rendered, recreated as a divIcon so
+// we can drop it on a real L.Marker (CircleMarker has no `dragging`
+// handler and can't be made draggable).
+const dotIcon = L.divIcon({
+ html:
+ '
',
+ className: "rover-location-dot",
+ iconSize: [16, 16],
+ iconAnchor: [8, 8]
+})
+
+const draggingDotIcon = L.divIcon({
+ html:
+ '',
+ className: "rover-location-dot rover-location-dot-edit",
+ iconSize: [18, 18],
+ iconAnchor: [9, 9]
+})
+
export const LocationMap: Partial = {
mounted(this: LocationMapHook) {
const lat = parseFloat(this.el.dataset.lat || "0")
@@ -41,16 +60,23 @@ export const LocationMap: Partial = {
{ position: "topright", collapsed: false }
).addTo(map)
- this.marker = L.circleMarker([lat, lon], {
- radius: 8,
- color: "#ffffff",
- weight: 2,
- fillColor: "#ef4444",
- fillOpacity: 0.95,
- pane: "markerPane"
- }).addTo(map)
+ // Draggable Marker (not CircleMarker — that one can't be dragged).
+ // Server flips draggability via `set_marker_draggable` push_event.
+ this.marker = L.marker([lat, lon], { icon: dotIcon, draggable: false }).addTo(map)
this.marker.bindPopup(`${lat.toFixed(6)}, ${lon.toFixed(6)}`)
+ this.marker.on("drag", (e: L.LeafletEvent) => {
+ const ll = (e.target as L.Marker).getLatLng()
+ this.marker.setPopupContent(`${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}`)
+ })
+
+ // dragend pushes the new coords up so the LiveView can update its
+ // `working_lat` / `working_lon` and render the live grid preview.
+ this.marker.on("dragend", (e: L.DragEndEvent) => {
+ const ll = (e.target as L.Marker).getLatLng()
+ this.pushEvent("location_dragged", { lat: ll.lat, lon: ll.lng })
+ })
+
// Maidenhead grid overlay + toggle
this.gridLayer = L.layerGroup().addTo(map)
this.gridVisible = true
@@ -84,6 +110,22 @@ export const LocationMap: Partial = {
})
new GridToggle().addTo(map)
+ // Server → hook events for edit mode.
+ this.handleEvent("set_marker_draggable", ({ draggable }: { draggable: boolean }) => {
+ if (draggable) {
+ this.marker.setIcon(draggingDotIcon)
+ this.marker.dragging?.enable()
+ } else {
+ this.marker.setIcon(dotIcon)
+ this.marker.dragging?.disable()
+ }
+ })
+
+ this.handleEvent("reset_marker", ({ lat, lon }: { lat: number; lon: number }) => {
+ this.marker.setLatLng([lat, lon])
+ this.marker.setPopupContent(`${lat.toFixed(6)}, ${lon.toFixed(6)}`)
+ })
+
// LiveView reconnect / tab visibility — Leaflet needs invalidateSize.
this.visibilityHandler = () => {
if (document.visibilityState === "visible") this.map.invalidateSize()
diff --git a/lib/microwaveprop_web/live/rover_locations_live/show.ex b/lib/microwaveprop_web/live/rover_locations_live/show.ex
index a3e1d98e..cbf88005 100644
--- a/lib/microwaveprop_web/live/rover_locations_live/show.ex
+++ b/lib/microwaveprop_web/live/rover_locations_live/show.ex
@@ -16,6 +16,9 @@ defmodule MicrowavepropWeb.RoverLocationsLive.Show do
assign(socket,
page_title: "Rover Location",
location: loc,
+ editing: false,
+ working_lat: loc.lat,
+ working_lon: loc.lon,
grid: Maidenhead.from_latlon(loc.lat, loc.lon, 10)
)}
@@ -28,6 +31,76 @@ defmodule MicrowavepropWeb.RoverLocationsLive.Show do
end
@impl true
+ def handle_event("toggle_edit", _params, socket) do
+ if can_modify?(socket.assigns[:current_scope], socket.assigns.location) do
+ {:noreply,
+ socket
+ |> assign(editing: true)
+ |> push_event("set_marker_draggable", %{draggable: true})}
+ else
+ {:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
+ end
+ end
+
+ def handle_event("cancel_edit", _params, socket) do
+ loc = socket.assigns.location
+
+ {:noreply,
+ socket
+ |> assign(
+ editing: false,
+ working_lat: loc.lat,
+ working_lon: loc.lon,
+ grid: Maidenhead.from_latlon(loc.lat, loc.lon, 10)
+ )
+ |> push_event("reset_marker", %{lat: loc.lat, lon: loc.lon})
+ |> push_event("set_marker_draggable", %{draggable: false})}
+ end
+
+ # Hook fires this on `dragend` with the marker's new coordinates.
+ # We only update the working preview — persistence waits for save.
+ def handle_event("location_dragged", %{"lat" => lat, "lon" => lon}, socket) when is_number(lat) and is_number(lon) do
+ {:noreply,
+ assign(socket,
+ working_lat: lat,
+ working_lon: lon,
+ grid: Maidenhead.from_latlon(lat, lon, 10)
+ )}
+ end
+
+ def handle_event("save_edit", _params, socket) do
+ case current_user(socket.assigns[:current_scope]) do
+ %User{} = user ->
+ attrs = %{lat: socket.assigns.working_lat, lon: socket.assigns.working_lon}
+
+ case Rover.update_location(user, socket.assigns.location.id, attrs) do
+ {:ok, updated} ->
+ updated = Repo.preload(updated, :user)
+
+ {:noreply,
+ socket
+ |> assign(
+ location: updated,
+ editing: false,
+ working_lat: updated.lat,
+ working_lon: updated.lon,
+ grid: Maidenhead.from_latlon(updated.lat, updated.lon, 10)
+ )
+ |> put_flash(:info, "Location updated.")
+ |> push_event("set_marker_draggable", %{draggable: false})}
+
+ {:error, :not_found} ->
+ {:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
+
+ {:error, %Ecto.Changeset{}} ->
+ {:noreply, put_flash(socket, :error, "Could not save those coordinates.")}
+ end
+
+ _ ->
+ {:noreply, put_flash(socket, :error, "Sign in required.")}
+ end
+ end
+
def handle_event("delete", _params, socket) do
case current_user(socket.assigns[:current_scope]) do
%User{} = user ->
@@ -86,13 +159,42 @@ defmodule MicrowavepropWeb.RoverLocationsLive.Show do
<:subtitle>
{status_label(@location.status)}
{@grid}
+ Editing — drag the marker
<:actions>
<.link navigate={~p"/rover-locations"} class="btn btn-ghost btn-sm">
<.icon name="hero-arrow-left" class="w-4 h-4" /> Back
+
+
+
+
+
+
+