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 + + + + + + +