diff --git a/assets/js/propagation_map_hook.js b/assets/js/propagation_map_hook.js index df980e35..0b6fbaf2 100644 --- a/assets/js/propagation_map_hook.js +++ b/assets/js/propagation_map_hook.js @@ -195,11 +195,6 @@ function isMobile() { return window.innerWidth < 768 } -function closeButtonHTML() { - if (!isMobile()) return "" - return `` -} - function mobileHandleHTML() { if (!isMobile()) return "" return `
` @@ -208,10 +203,13 @@ function mobileHandleHTML() { function buildLoadingHTML(detail) { const tier = scoreTier(detail.score) return `
- ${mobileHandleHTML()}${closeButtonHTML()} -
- ${detail.score}/100 - ${tier.label} + ${mobileHandleHTML()} +
+
+ ${detail.score}/100 + ${tier.label} +
+
Loading… @@ -325,10 +323,13 @@ function buildPopupHTML(detail, viewshedLoading) { const minW = mobile ? "auto" : "260px" return `
- ${mobileHandleHTML()}${closeButtonHTML()} -
- ${detail.score}/100 - ${tier.label} + ${mobileHandleHTML()} +
+
+ ${detail.score}/100 + ${tier.label} +
+
@@ -398,12 +399,22 @@ export const PropagationMap = { this.bandInfo = band_info }) - // Prevent control panel from eating map clicks/scrolls + // Prevent control panel / sidebar from eating map clicks/scrolls const controls = document.getElementById("map-controls") if (controls) { L.DomEvent.disableClickPropagation(controls) L.DomEvent.disableScrollPropagation(controls) } + const sidebar = document.getElementById("sidebar") + if (sidebar) { + L.DomEvent.disableClickPropagation(sidebar) + L.DomEvent.disableScrollPropagation(sidebar) + } + + // Invalidate map size when sidebar is toggled + window.addEventListener("sidebar-toggle", () => { + setTimeout(() => this.map.invalidateSize(), 50) + }) // Grid overlay layer this.gridLayer = L.layerGroup() @@ -447,20 +458,25 @@ export const PropagationMap = { if (basic && this.detailPanel) { const merged = { ...basic, ...this.bandInfo, factors: null } this.detailPanel.innerHTML = buildLoadingHTML(merged) - this.detailPanel.style.display = "block" - this.escHint.style.opacity = "1" - this.positionDetailPanel() + this.showDetailPanel() this.pushEvent("point_detail", { lat: e.latlng.lat, lon: e.latlng.lng }) } }) - // Detail panel — bottom sheet on mobile, positioned next to sidebar on desktop + // Detail panel — floating overlay on the map this.detailPanel = document.getElementById("detail-panel") this.viewshedLoading = false if (this.detailPanel) { L.DomEvent.disableClickPropagation(this.detailPanel) L.DomEvent.disableScrollPropagation(this.detailPanel) + + // Close button event delegation + this.detailPanel.addEventListener("click", (e) => { + if (e.target.closest(".detail-close-btn")) { + this.hideDetailPanel() + } + }) } this.lastDetail = null @@ -469,9 +485,7 @@ export const PropagationMap = { const merged = { ...detail, ...this.bandInfo } this.lastDetail = merged this.detailPanel.innerHTML = buildPopupHTML(merged, this.viewshedLoading) - this.detailPanel.style.display = "block" - this.escHint.style.opacity = "1" - this.positionDetailPanel() + this.showDetailPanel() // Draw propagation reach polygon based on contiguous good cells if (this.gridLookup && this.clickedLatLng) { @@ -563,24 +577,10 @@ export const PropagationMap = { L.DomEvent.disableScrollPropagation(this.timelineEl) } - // ESC hint — shown on desktop when detail panel is visible - this.escHint = document.createElement("div") - this.escHint.className = "hidden md:block" - this.escHint.style.cssText = "position:absolute;top:12px;right:12px;z-index:1100;background:rgba(0,0,0,0.6);color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;opacity:0;transition:opacity 0.2s;pointer-events:none;" - this.escHint.textContent = "Press ESC to close" - this.el.appendChild(this.escHint) - // Escape key closes detail panel and range circles document.addEventListener("keydown", (e) => { if (e.key === "Escape") { - if (this.detailPanel) { - this.detailPanel.style.display = "none" - this.detailPanel.innerHTML = "" - } - this.rangeCircles.clearLayers() - this.clickedLatLng = null - this.lastDetail = null - this.escHint.style.opacity = "0" + this.hideDetailPanel() } }) @@ -623,15 +623,19 @@ export const PropagationMap = { legend.addTo(this.map) }, - positionDetailPanel() { - if (!this.detailPanel || isMobile()) return - // On desktop, position the panel below the control panel - const controls = document.getElementById("map-controls") - if (controls) { - const rect = controls.getBoundingClientRect() - this.detailPanel.style.left = rect.left + "px" - this.detailPanel.style.top = (rect.bottom + 8) + "px" + showDetailPanel() { + if (!this.detailPanel) return + this.detailPanel.style.display = "block" + }, + + hideDetailPanel() { + if (this.detailPanel) { + this.detailPanel.style.display = "none" + this.detailPanel.innerHTML = "" } + this.rangeCircles.clearLayers() + this.clickedLatLng = null + this.lastDetail = null }, renderScores(scores) { diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 485c20e6..fc8c02a3 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -256,56 +256,194 @@ defmodule MicrowavepropWeb.MapLive do @impl true def render(assigns) do ~H""" -
- <%!-- Full-page map --%> -
- %{time: DateTime.to_iso8601(t), label: format_time_label(t)} - end) - ) - } - data-selected-time={if @selected_time, do: DateTime.to_iso8601(@selected_time), else: ""} - class="absolute inset-0 z-0" - > +
+ <%!-- Map container --%> +
+ <%!-- Full-page map --%> +
+ %{time: DateTime.to_iso8601(t), label: format_time_label(t)} + end) + ) + } + data-selected-time={if @selected_time, do: DateTime.to_iso8601(@selected_time), else: ""} + class="absolute inset-0 z-0" + > +
+ + <%!-- Mobile-only floating controls --%> +
+
+
+
+ NTMS +
Propagation Map
+
+
+
+
+ +
+
+ + + + +
+
+ + <%!-- Point detail floating panel --%> + + + <%!-- Bottom timeline bar --%> +
+
+ + <%!-- Sidebar expand button (visible when sidebar is collapsed) --%> +
- <%!-- Top-left control panel --%> + <%!-- Desktop right sidebar --%>
-
+ <%!-- Sidebar controls --%> +
- - NTMS + NTMS
Propagation Map
- <%!-- Band selector (always visible) --%> + <%!-- Band selector --%> - <%!-- Secondary controls (collapsed on mobile by default) --%> - + <%!-- Navigation --%> + + + <%!-- Auth --%> +
- - <%!-- Point detail panel — bottom sheet on mobile, sidebar on desktop --%> - - - <%!-- Bottom timeline bar --%> -
-
diff --git a/lib/microwaveprop_web/router.ex b/lib/microwaveprop_web/router.ex index abc297d8..0af5b33d 100644 --- a/lib/microwaveprop_web/router.ex +++ b/lib/microwaveprop_web/router.ex @@ -46,20 +46,22 @@ defmodule MicrowavepropWeb.Router do get "/", PageController, :home - live "/submit", SubmitLive - live "/map", MapLive - live "/weather", WeatherMapLive - live "/contacts", ContactLive.Index - live "/contacts/map", ContactMapLive - live "/contacts/:id", ContactLive.Show - live "/path", PathLive - live "/rover", RoverLive - live "/algo", AlgoLive - live "/backfill", BackfillLive + live_session :public, on_mount: [{MicrowavepropWeb.UserAuth, :default}] do + live "/submit", SubmitLive + live "/map", MapLive + live "/weather", WeatherMapLive + live "/contacts", ContactLive.Index + live "/contacts/map", ContactMapLive + live "/contacts/:id", ContactLive.Show + live "/path", PathLive + live "/rover", RoverLive + live "/algo", AlgoLive + live "/backfill", BackfillLive - # Beacons: read-only public views - live "/beacons", BeaconLive.Index, :index - live "/beacons/:id", BeaconLive.Show, :show + # Beacons: read-only public views + live "/beacons", BeaconLive.Index, :index + live "/beacons/:id", BeaconLive.Show, :show + end # Redirect old /qsos routes get "/qsos", PageController, :redirect_contacts