/beacons gains a Leaflet map above the table showing every approved beacon as a circle marker with a popup that links to the detail page. New BeaconsListMap JS hook (assets/js/beacons_list_map_hook.ts) walks a data-beacons JSON payload, fitBounds over the marker set, and mirrors the format_freq/1 integer-comma formatting used on the detail view so the popups match the rest of the UI. Approved/on-air beacons render green, off-air beacons render gray. BeaconLive.Index.mount/3 now materializes the list into an assign so it can encode it into the data attribute; the live PubSub path keeps the map in sync on create/update/delete. Profile (/u/:callsign) polish: drop the hero-radio, hero-signal, and hero-information-circle icons per request, and replace the daisyUI `avatar placeholder` wrapper with a plain flex-centered circle so the initial letter actually lives inside the circle instead of anchored to the top-left corner (daisyUI 5 removed the old placeholder-centering behavior). Navigation ordering: in the top navbar (layouts.ex) Contacts now sits immediately before Contact Map instead of after it. All three vertical sidebars already had them adjacent in the correct order, so only the horizontal nav changed.
162 lines
6 KiB
TypeScript
162 lines
6 KiB
TypeScript
// Leaflet map library (vendored)
|
|
import * as L from "../vendor/leaflet/leaflet.js"
|
|
window.L = L
|
|
|
|
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
|
import "phoenix_html"
|
|
// Establish Phoenix Socket and LiveView configuration.
|
|
import {Socket} from "phoenix"
|
|
import {LiveSocket} from "phoenix_live_view"
|
|
import {hooks as colocatedHooks} from "phoenix-colocated/microwaveprop"
|
|
import topbar from "../vendor/topbar"
|
|
|
|
import initLiveStash from "../../deps/live_stash/assets/js/live-stash.js"
|
|
import {PropagationMap} from "./propagation_map_hook"
|
|
import {ContactMap} from "./contact_map_hook"
|
|
import {ContactsMap} from "./contacts_map_hook"
|
|
import {ElevationProfile} from "./elevation_profile_hook"
|
|
import {WeatherMap} from "./weather_map_hook"
|
|
import {LocateMe, CopyLink} from "./locate_me_hook"
|
|
import {RoverMap} from "./rover_map_hook"
|
|
import {BeaconMap} from "./beacon_map_hook"
|
|
import {BeaconsListMap} from "./beacons_list_map_hook"
|
|
|
|
interface UtcClockHook extends ViewHook {
|
|
timer: ReturnType<typeof setInterval>
|
|
tick(): void
|
|
}
|
|
|
|
const UtcClock = {
|
|
mounted(this: UtcClockHook) {
|
|
this.tick()
|
|
this.timer = setInterval(() => this.tick(), 10_000)
|
|
},
|
|
tick(this: UtcClockHook) {
|
|
const now = new Date()
|
|
const h = now.getUTCHours().toString().padStart(2, "0")
|
|
const m = now.getUTCMinutes().toString().padStart(2, "0")
|
|
this.el.textContent = `${h}:${m} UTC`
|
|
},
|
|
destroyed(this: UtcClockHook) {
|
|
clearInterval(this.timer)
|
|
}
|
|
}
|
|
|
|
interface CommaNumberHook extends ViewHook {
|
|
format(): void
|
|
}
|
|
|
|
const CommaNumber = {
|
|
mounted(this: CommaNumberHook) {
|
|
this.el.addEventListener("input", () => this.format())
|
|
this.format()
|
|
},
|
|
updated(this: CommaNumberHook) { this.format() },
|
|
format(this: CommaNumberHook) {
|
|
const input = (this.el.querySelector("input") || this.el) as HTMLInputElement
|
|
const raw = input.value.replace(/,/g, "")
|
|
if (raw === "" || raw === "-") return
|
|
const cursor = input.selectionStart ?? 0
|
|
const commasBefore = (input.value.slice(0, cursor).match(/,/g) || []).length
|
|
const parts = raw.split(".")
|
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
|
const formatted = parts.join(".")
|
|
if (formatted !== input.value) {
|
|
input.value = formatted
|
|
const commasAfter = (formatted.slice(0, cursor).match(/,/g) || []).length
|
|
const newCursor = cursor + (commasAfter - commasBefore)
|
|
input.setSelectionRange(newCursor, newCursor)
|
|
}
|
|
}
|
|
}
|
|
|
|
const csrfToken = document.querySelector("meta[name='csrf-token']")!.getAttribute("content")
|
|
const liveSocket = new LiveSocket("/live", Socket, {
|
|
longPollFallbackMs: 2500,
|
|
params: initLiveStash({_csrf_token: csrfToken}),
|
|
hooks: {...colocatedHooks, PropagationMap, UtcClock, ContactMap, ContactsMap, ElevationProfile, WeatherMap, LocateMe, CopyLink, RoverMap, BeaconMap, BeaconsListMap, CommaNumber},
|
|
})
|
|
|
|
// Show progress bar on live navigation and form submits
|
|
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
|
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
|
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
|
|
|
// Delay disconnect alerts by 3 seconds so brief hiccups don't flash errors
|
|
let disconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
const DISCONNECT_DELAY_MS = 3000
|
|
|
|
function showDisconnectAlert(id: string) {
|
|
const el = document.getElementById(id)
|
|
if (el) el.removeAttribute("hidden")
|
|
}
|
|
|
|
function hideDisconnectAlert(id: string) {
|
|
const el = document.getElementById(id)
|
|
if (el) el.setAttribute("hidden", "")
|
|
}
|
|
|
|
new MutationObserver(() => {
|
|
const root = document.querySelector("[data-phx-main]")
|
|
if (!root) return
|
|
|
|
const hasError = root.classList.contains("phx-client-error") ||
|
|
root.classList.contains("phx-server-error")
|
|
|
|
if (hasError && !disconnectTimer) {
|
|
disconnectTimer = setTimeout(() => {
|
|
const r = document.querySelector("[data-phx-main]")
|
|
if (r?.classList.contains("phx-client-error")) showDisconnectAlert("client-error")
|
|
if (r?.classList.contains("phx-server-error")) showDisconnectAlert("server-error")
|
|
}, DISCONNECT_DELAY_MS)
|
|
} else if (!hasError && disconnectTimer) {
|
|
clearTimeout(disconnectTimer)
|
|
disconnectTimer = null
|
|
hideDisconnectAlert("client-error")
|
|
hideDisconnectAlert("server-error")
|
|
}
|
|
}).observe(document.body, {subtree: true, attributes: true, attributeFilter: ["class"]})
|
|
|
|
// connect if there are any LiveViews on the page
|
|
liveSocket.connect()
|
|
|
|
// expose liveSocket on window for web console debug logs and latency simulation:
|
|
// >> liveSocket.enableDebug()
|
|
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
|
|
// >> liveSocket.disableLatencySim()
|
|
window.liveSocket = liveSocket
|
|
|
|
// The lines below enable quality of life phoenix_live_reload
|
|
// development features:
|
|
//
|
|
// 1. stream server logs to the browser console
|
|
// 2. click on elements to jump to their definitions in your code editor
|
|
//
|
|
if (process.env.NODE_ENV === "development") {
|
|
window.addEventListener("phx:live_reload:attached", ({detail: reloader}: any) => {
|
|
// Enable server log streaming to client.
|
|
// Disable with reloader.disableServerLogs()
|
|
reloader.enableServerLogs()
|
|
|
|
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
|
|
//
|
|
// * click with "c" key pressed to open at caller location
|
|
// * click with "d" key pressed to open at function component definition location
|
|
let keyDown: string | null
|
|
window.addEventListener("keydown", (e: KeyboardEvent) => keyDown = e.key)
|
|
window.addEventListener("keyup", (_e: KeyboardEvent) => keyDown = null)
|
|
window.addEventListener("click", (e: MouseEvent) => {
|
|
if(keyDown === "c"){
|
|
e.preventDefault()
|
|
e.stopImmediatePropagation()
|
|
reloader.openEditorAtCaller(e.target)
|
|
} else if(keyDown === "d"){
|
|
e.preventDefault()
|
|
e.stopImmediatePropagation()
|
|
reloader.openEditorAtDef(e.target)
|
|
}
|
|
}, true)
|
|
|
|
window.liveReloader = reloader
|
|
})
|
|
}
|