Dock map controls to right sidebar with collapsible panel
- Move controls (band selector, grid toggle, nav links) to a docked right sidebar on desktop, keep floating overlay on mobile - Point detail panel floats over the map bottom-left instead of inside the narrow sidebar - Add collapse/expand toggle for the sidebar - Add auth links (login/logout/settings) to sidebar - Wrap public live routes in live_session with UserAuth on_mount - Use daisyUI menu lists for sidebar navigation
This commit is contained in:
parent
ee9275e0b9
commit
e4fb422402
3 changed files with 300 additions and 160 deletions
|
|
@ -195,11 +195,6 @@ function isMobile() {
|
|||
return window.innerWidth < 768
|
||||
}
|
||||
|
||||
function closeButtonHTML() {
|
||||
if (!isMobile()) return ""
|
||||
return `<button onclick="document.getElementById('detail-panel').style.display='none'" style="position:absolute;top:8px;right:10px;background:rgba(255,255,255,0.15);border:none;color:#fff;font-size:18px;line-height:1;width:28px;height:28px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;">×</button>`
|
||||
}
|
||||
|
||||
function mobileHandleHTML() {
|
||||
if (!isMobile()) return ""
|
||||
return `<div style="display:flex;justify-content:center;padding:6px 0 2px;"><div style="width:32px;height:4px;border-radius:2px;background:rgba(255,255,255,0.3);"></div></div>`
|
||||
|
|
@ -208,10 +203,13 @@ function mobileHandleHTML() {
|
|||
function buildLoadingHTML(detail) {
|
||||
const tier = scoreTier(detail.score)
|
||||
return `<div style="position:relative;">
|
||||
${mobileHandleHTML()}${closeButtonHTML()}
|
||||
<div style="background:${tier.color};color:#000;padding:6px 10px;display:flex;justify-content:space-between;align-items:baseline;">
|
||||
<strong style="font-size:16px;">${detail.score}/100</strong>
|
||||
<span style="font-size:13px;font-weight:700;">${tier.label}</span>
|
||||
${mobileHandleHTML()}
|
||||
<div style="background:${tier.color};color:#000;padding:6px 10px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<div style="display:flex;align-items:baseline;gap:8px;">
|
||||
<strong style="font-size:16px;">${detail.score}/100</strong>
|
||||
<span style="font-size:13px;font-weight:700;">${tier.label}</span>
|
||||
</div>
|
||||
<button class="detail-close-btn" style="background:rgba(0,0,0,0.2);border:none;color:#000;font-size:16px;line-height:1;width:24px;height:24px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;" title="Close">×</button>
|
||||
</div>
|
||||
<div style="padding:8px 10px;font-size:12px;color:#999;display:flex;align-items:center;gap:6px;">
|
||||
<span class="loading loading-spinner loading-xs"></span> Loading…
|
||||
|
|
@ -325,10 +323,13 @@ function buildPopupHTML(detail, viewshedLoading) {
|
|||
const minW = mobile ? "auto" : "260px"
|
||||
|
||||
return `<div style="min-width:${minW};position:relative;">
|
||||
${mobileHandleHTML()}${closeButtonHTML()}
|
||||
<div style="background:${tier.color};color:#000;padding:6px 10px;display:flex;justify-content:space-between;align-items:baseline;">
|
||||
<strong style="font-size:16px;">${detail.score}/100</strong>
|
||||
<span style="font-size:13px;font-weight:700;">${tier.label}</span>
|
||||
${mobileHandleHTML()}
|
||||
<div style="background:${tier.color};color:#000;padding:6px 10px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<div style="display:flex;align-items:baseline;gap:8px;">
|
||||
<strong style="font-size:16px;">${detail.score}/100</strong>
|
||||
<span style="font-size:13px;font-weight:700;">${tier.label}</span>
|
||||
</div>
|
||||
<button class="detail-close-btn" style="background:rgba(0,0,0,0.2);border:none;color:#000;font-size:16px;line-height:1;width:24px;height:24px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;" title="Close">×</button>
|
||||
</div>
|
||||
<div style="padding:6px 10px;">
|
||||
<div style="font-size:12px;opacity:0.7;margin-bottom:6px;">
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -256,56 +256,194 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="relative w-screen h-screen overflow-hidden">
|
||||
<%!-- Full-page map --%>
|
||||
<div
|
||||
id="propagation-map"
|
||||
phx-hook="PropagationMap"
|
||||
phx-update="ignore"
|
||||
data-scores={@initial_scores_json}
|
||||
data-band-info={Jason.encode!(band_info(@selected_band))}
|
||||
data-valid-times={
|
||||
Jason.encode!(
|
||||
Enum.map(@valid_times, fn t ->
|
||||
%{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"
|
||||
>
|
||||
<div class="flex w-screen h-screen overflow-hidden">
|
||||
<%!-- Map container --%>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<%!-- Full-page map --%>
|
||||
<div
|
||||
id="propagation-map"
|
||||
phx-hook="PropagationMap"
|
||||
phx-update="ignore"
|
||||
data-scores={@initial_scores_json}
|
||||
data-band-info={Jason.encode!(band_info(@selected_band))}
|
||||
data-valid-times={
|
||||
Jason.encode!(
|
||||
Enum.map(@valid_times, fn t ->
|
||||
%{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"
|
||||
>
|
||||
</div>
|
||||
|
||||
<%!-- Mobile-only floating controls --%>
|
||||
<div
|
||||
id="map-controls"
|
||||
class="md:hidden absolute top-2 left-12 z-[1000] flex flex-col gap-2 max-w-[calc(100vw-4rem)]"
|
||||
>
|
||||
<div class="bg-base-100/90 shadow rounded-box border border-base-300 p-2 flex flex-col gap-2">
|
||||
<div class="font-bold text-sm leading-tight px-1 flex items-center justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<span>NTMS</span>
|
||||
<div class="font-normal text-xs opacity-70">Propagation Map</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
<div
|
||||
id="utc-clock"
|
||||
phx-hook="UtcClock"
|
||||
class="font-mono text-xs opacity-70 tabular-nums"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
id="mobile-panel-toggle"
|
||||
class="btn btn-xs btn-square btn-ghost"
|
||||
onclick="document.getElementById('panel-extras').classList.toggle('hidden')"
|
||||
>
|
||||
<.icon name="hero-bars-3" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-sm w-full justify-between">
|
||||
{selected_label(@bands, @selected_band)}
|
||||
<.icon name="hero-chevron-down" class="size-3" />
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box shadow-lg w-44 mt-1 p-1"
|
||||
>
|
||||
<li :for={band <- @bands}>
|
||||
<button
|
||||
onclick="document.activeElement.blur()"
|
||||
phx-click={
|
||||
JS.dispatch("show-loading", to: "#propagation-map")
|
||||
|> JS.push("select_band", value: %{value: band.freq_mhz})
|
||||
}
|
||||
class={[if(@selected_band == band.freq_mhz, do: "active")]}
|
||||
>
|
||||
{band.label}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="panel-extras" class="hidden flex-col gap-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer px-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm toggle-primary"
|
||||
checked={@grid_visible}
|
||||
phx-click="toggle_grid"
|
||||
/>
|
||||
<span class="text-sm">Grid squares</span>
|
||||
</label>
|
||||
|
||||
<form phx-change="set_antenna_height" class="flex items-center gap-2 px-1">
|
||||
<label class="text-sm whitespace-nowrap">Antenna height</label>
|
||||
<input
|
||||
type="number"
|
||||
name="height_ft"
|
||||
min="0"
|
||||
max="200"
|
||||
step="1"
|
||||
value={@antenna_height_ft}
|
||||
class="input input-xs w-16 text-right"
|
||||
/>
|
||||
<span class="text-xs opacity-70">ft</span>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col gap-1 border-t border-base-300 pt-2">
|
||||
<.link navigate="/path" class="btn btn-xs btn-ghost justify-start">
|
||||
Path Calculator
|
||||
</.link>
|
||||
<.link navigate="/rover" class="btn btn-xs btn-ghost justify-start">
|
||||
Rover Planner
|
||||
</.link>
|
||||
<.link navigate="/weather" class="btn btn-xs btn-ghost justify-start">
|
||||
Weather Map
|
||||
</.link>
|
||||
<.link navigate="/algo" class="btn btn-xs btn-ghost justify-start">
|
||||
Scoring Algorithm
|
||||
</.link>
|
||||
<.link navigate="/submit" class="btn btn-xs btn-ghost justify-start">
|
||||
Submit a Contact
|
||||
</.link>
|
||||
<.link navigate="/contacts" class="btn btn-xs btn-ghost justify-start">
|
||||
Contact Training Data
|
||||
</.link>
|
||||
<.link navigate="/contacts/map" class="btn btn-xs btn-ghost justify-start">
|
||||
Contact Map
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Point detail floating panel --%>
|
||||
<div
|
||||
id="detail-panel"
|
||||
phx-update="ignore"
|
||||
class={[
|
||||
"bg-neutral text-neutral-content shadow-lg border border-base-300 overflow-hidden overflow-y-auto z-[1001]",
|
||||
"fixed bottom-0 left-0 right-0 max-h-[50vh] rounded-t-2xl",
|
||||
"md:absolute md:top-auto md:right-auto md:bottom-14 md:left-3 md:max-h-[70vh] md:w-80 md:rounded-box"
|
||||
]}
|
||||
style="display:none;"
|
||||
>
|
||||
</div>
|
||||
|
||||
<%!-- Bottom timeline bar --%>
|
||||
<div
|
||||
id="forecast-timeline"
|
||||
phx-update="ignore"
|
||||
class="absolute bottom-2 left-1/2 -translate-x-1/2 z-[1000] max-w-[calc(100vw-1rem)] md:bottom-4"
|
||||
>
|
||||
</div>
|
||||
|
||||
<%!-- Sidebar expand button (visible when sidebar is collapsed) --%>
|
||||
<button
|
||||
id="sidebar-expand"
|
||||
class="hidden md:hidden absolute top-3 right-3 z-[1000] btn btn-sm btn-neutral shadow-lg"
|
||||
onclick="document.getElementById('sidebar').style.display='flex';this.style.display='none';window.dispatchEvent(new Event('sidebar-toggle'));"
|
||||
>
|
||||
<.icon name="hero-bars-3" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%!-- Top-left control panel --%>
|
||||
<%!-- Desktop right sidebar --%>
|
||||
<div
|
||||
id="map-controls"
|
||||
class="absolute top-2 left-12 z-[1000] flex flex-col gap-2 max-w-[calc(100vw-4rem)] md:left-14 md:top-3 md:max-w-none"
|
||||
id="sidebar"
|
||||
class="hidden md:flex flex-col w-56 shrink-0 h-full bg-neutral text-neutral-content border-l border-base-300 overflow-hidden"
|
||||
>
|
||||
<div class="bg-base-100/90 shadow rounded-box border border-base-300 p-2 md:p-3 flex flex-col gap-2">
|
||||
<%!-- Sidebar controls --%>
|
||||
<div class="p-3 flex flex-col gap-2 shrink-0">
|
||||
<div class="font-bold text-sm leading-tight px-1 flex items-center justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<span class="hidden md:inline">North Texas Microwave Society</span>
|
||||
<span class="md:hidden">NTMS</span>
|
||||
<span>NTMS</span>
|
||||
<div class="font-normal text-xs opacity-70">Propagation Map</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
<div
|
||||
id="utc-clock"
|
||||
id="utc-clock-desktop"
|
||||
phx-hook="UtcClock"
|
||||
class="font-mono text-xs opacity-70 tabular-nums"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
id="mobile-panel-toggle"
|
||||
class="btn btn-xs btn-square btn-ghost md:hidden"
|
||||
onclick="document.getElementById('panel-extras').classList.toggle('hidden')"
|
||||
id="sidebar-collapse"
|
||||
class="btn btn-xs btn-square btn-ghost"
|
||||
title="Collapse sidebar"
|
||||
onclick="document.getElementById('sidebar').style.display='none';document.getElementById('sidebar-expand').style.display='flex';window.dispatchEvent(new Event('sidebar-toggle'));"
|
||||
>
|
||||
<.icon name="hero-bars-3" class="size-4" />
|
||||
<.icon name="hero-chevron-right" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Band selector (always visible) --%>
|
||||
<%!-- Band selector --%>
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-sm w-full justify-between">
|
||||
{selected_label(@bands, @selected_band)}
|
||||
|
|
@ -313,7 +451,7 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box shadow-lg w-44 mt-1 p-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box shadow-lg w-44 mt-1 p-1 z-10"
|
||||
>
|
||||
<li :for={band <- @bands}>
|
||||
<button
|
||||
|
|
@ -330,82 +468,78 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<%!-- Secondary controls (collapsed on mobile by default) --%>
|
||||
<div id="panel-extras" class="hidden md:flex flex-col gap-2">
|
||||
<%!-- Grid toggle --%>
|
||||
<label class="flex items-center gap-2 cursor-pointer px-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm toggle-primary"
|
||||
checked={@grid_visible}
|
||||
phx-click="toggle_grid"
|
||||
/>
|
||||
<span class="text-sm">Grid squares</span>
|
||||
</label>
|
||||
<%!-- Grid toggle --%>
|
||||
<label class="flex items-center gap-2 cursor-pointer px-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm toggle-primary"
|
||||
checked={@grid_visible}
|
||||
phx-click="toggle_grid"
|
||||
/>
|
||||
<span class="text-sm">Grid squares</span>
|
||||
</label>
|
||||
|
||||
<%!-- Antenna height --%>
|
||||
<form phx-change="set_antenna_height" class="flex items-center gap-2 px-1">
|
||||
<label class="text-sm whitespace-nowrap">Antenna height</label>
|
||||
<input
|
||||
type="number"
|
||||
name="height_ft"
|
||||
min="0"
|
||||
max="200"
|
||||
step="1"
|
||||
value={@antenna_height_ft}
|
||||
class="input input-xs w-16 text-right"
|
||||
/>
|
||||
<span class="text-xs opacity-70">ft</span>
|
||||
</form>
|
||||
<%!-- Antenna height --%>
|
||||
<form phx-change="set_antenna_height" class="flex items-center gap-2 px-1">
|
||||
<label class="text-sm whitespace-nowrap">Antenna height</label>
|
||||
<input
|
||||
type="number"
|
||||
name="height_ft"
|
||||
min="0"
|
||||
max="200"
|
||||
step="1"
|
||||
value={@antenna_height_ft}
|
||||
class="input input-xs w-16 text-right"
|
||||
/>
|
||||
<span class="text-xs opacity-70">ft</span>
|
||||
</form>
|
||||
|
||||
<%!-- Links --%>
|
||||
<div class="flex flex-col gap-1 border-t border-base-300 pt-2">
|
||||
<.link navigate="/path" class="btn btn-xs btn-ghost justify-start">
|
||||
Path Calculator
|
||||
</.link>
|
||||
<.link navigate="/rover" class="btn btn-xs btn-ghost justify-start">
|
||||
Rover Planner
|
||||
</.link>
|
||||
<.link navigate="/weather" class="btn btn-xs btn-ghost justify-start">
|
||||
Weather Map
|
||||
</.link>
|
||||
<.link navigate="/algo" class="btn btn-xs btn-ghost justify-start">
|
||||
Scoring Algorithm
|
||||
</.link>
|
||||
<.link navigate="/submit" class="btn btn-xs btn-ghost justify-start">
|
||||
Submit a Contact
|
||||
</.link>
|
||||
<.link navigate="/contacts" class="btn btn-xs btn-ghost justify-start">
|
||||
Contact Training Data
|
||||
</.link>
|
||||
<.link navigate="/contacts/map" class="btn btn-xs btn-ghost justify-start">
|
||||
Contact Map
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
<%!-- Navigation --%>
|
||||
<ul class="menu menu-xs border-t border-base-300 pt-2 px-0">
|
||||
<li>
|
||||
<.link navigate="/path">Path Calculator</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link navigate="/rover">Rover Planner</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link navigate="/weather">Weather Map</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link navigate="/algo">Scoring Algorithm</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link navigate="/submit">Submit a Contact</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link navigate="/contacts">Contact Training Data</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link navigate="/contacts/map">Contact Map</.link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<%!-- Auth --%>
|
||||
<ul class="menu menu-xs border-t border-base-300 pt-2 px-0">
|
||||
<%= if @current_scope && @current_scope.user do %>
|
||||
<li class="menu-title text-xs opacity-70">{@current_scope.user.callsign}</li>
|
||||
<li>
|
||||
<.link href={~p"/users/settings"}>Settings</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link href={~p"/users/log-out"} method="delete">Log out</.link>
|
||||
</li>
|
||||
<% else %>
|
||||
<li>
|
||||
<.link href={~p"/users/register"}>Register</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link href={~p"/users/log-in"}>Log in</.link>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Point detail panel — bottom sheet on mobile, sidebar on desktop --%>
|
||||
<div
|
||||
id="detail-panel"
|
||||
phx-update="ignore"
|
||||
class={[
|
||||
"bg-neutral text-neutral-content shadow-lg border border-base-300 overflow-hidden overflow-y-auto z-[1001]",
|
||||
"fixed bottom-0 left-0 right-0 max-h-[50vh] rounded-t-2xl",
|
||||
"md:absolute md:top-3 md:bottom-auto md:left-auto md:right-auto md:max-h-[60vh] md:max-w-sm md:rounded-box"
|
||||
]}
|
||||
style="display:none;"
|
||||
>
|
||||
</div>
|
||||
|
||||
<%!-- Bottom timeline bar --%>
|
||||
<div
|
||||
id="forecast-timeline"
|
||||
phx-update="ignore"
|
||||
class="absolute bottom-2 left-1/2 -translate-x-1/2 z-[1000] max-w-[calc(100vw-1rem)] md:bottom-4"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Layouts.flash_group flash={@flash} />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue