diff --git a/assets/js/app.js b/assets/js/app.js index e6b5d63f..bf822188 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -55,11 +55,35 @@ const UtcClock = { } } +const CommaNumber = { + mounted() { + this.el.addEventListener("input", () => this.format()) + this.format() + }, + updated() { this.format() }, + format() { + const input = this.el.querySelector("input") || this.el + const raw = input.value.replace(/,/g, "") + if (raw === "" || raw === "-") return + const cursor = input.selectionStart + 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}, + hooks: {...colocatedHooks, PropagationMap, UtcClock, ContactMap, ContactsMap, ElevationProfile, WeatherMap, LocateMe, CopyLink, RoverMap, BeaconMap, CommaNumber}, }) // Show progress bar on live navigation and form submits diff --git a/lib/microwaveprop/beacons.ex b/lib/microwaveprop/beacons.ex index 1d3ece79..afa2051f 100644 --- a/lib/microwaveprop/beacons.ex +++ b/lib/microwaveprop/beacons.ex @@ -28,12 +28,12 @@ defmodule Microwaveprop.Beacons do Phoenix.PubSub.broadcast(Microwaveprop.PubSub, @topic, message) end - @doc "Returns approved beacons ordered by frequency." + @doc "Returns approved beacons ordered by most recently added." def list_beacons do Repo.all( from b in Beacon, where: b.approved == true, - order_by: [asc: b.frequency_mhz, asc: b.callsign] + order_by: [desc: b.inserted_at] ) end diff --git a/lib/microwaveprop/beacons/beacon.ex b/lib/microwaveprop/beacons/beacon.ex index 247ea003..73927947 100644 --- a/lib/microwaveprop/beacons/beacon.ex +++ b/lib/microwaveprop/beacons/beacon.ex @@ -166,6 +166,9 @@ defmodule Microwaveprop.Beacons.Beacon do :notes ]) |> update_change(:callsign, fn cs -> cs && String.upcase(String.trim(cs)) end) + |> update_change(:lat, &round_coord/1) + |> update_change(:lon, &round_coord/1) + |> update_change(:height_ft, &round_int/1) |> maybe_fill_latlon() |> maybe_fill_grid() |> normalize_bearing_change() @@ -182,6 +185,14 @@ defmodule Microwaveprop.Beacons.Beacon do |> validate_grid_format() end + defp round_coord(nil), do: nil + defp round_coord(v) when is_float(v), do: Float.round(v, 6) + defp round_coord(v), do: v + + defp round_int(nil), do: nil + defp round_int(v) when is_float(v), do: Float.round(v, 0) + defp round_int(v), do: v + # If grid is blank but lat/lon are valid, derive it. defp maybe_fill_grid(changeset) do grid = get_field(changeset, :grid) diff --git a/lib/microwaveprop_web/live/beacon_live/form.ex b/lib/microwaveprop_web/live/beacon_live/form.ex index 9dbadd6f..1374d12b 100644 --- a/lib/microwaveprop_web/live/beacon_live/form.ex +++ b/lib/microwaveprop_web/live/beacon_live/form.ex @@ -20,9 +20,10 @@ defmodule MicrowavepropWeb.BeaconLive.Form do <.form for={@form} id="beacon-form" phx-change="validate" phx-submit="save"> <.input field={@form[:frequency_mhz]} - type="number" + type="text" label="Frequency (MHz)" - step="any" + inputmode="decimal" + phx-hook="CommaNumber" required /> <.input field={@form[:callsign]} type="text" label="Call" required /> @@ -105,12 +106,19 @@ defmodule MicrowavepropWeb.BeaconLive.Form do @impl true def handle_event("validate", %{"beacon" => beacon_params}, socket) do - changeset = Beacons.change_beacon(socket.assigns.beacon, beacon_params) + changeset = Beacons.change_beacon(socket.assigns.beacon, strip_commas(beacon_params)) {:noreply, assign(socket, form: to_form(changeset, action: :validate))} end def handle_event("save", %{"beacon" => beacon_params}, socket) do - save_beacon(socket, socket.assigns.live_action, beacon_params) + save_beacon(socket, socket.assigns.live_action, strip_commas(beacon_params)) + end + + defp strip_commas(params) do + Map.update(params, "frequency_mhz", nil, fn + v when is_binary(v) -> String.replace(v, ",", "") + v -> v + end) end defp save_beacon(socket, :edit, beacon_params) do diff --git a/lib/microwaveprop_web/live/beacon_live/index.ex b/lib/microwaveprop_web/live/beacon_live/index.ex index 2c9bea74..0ccfec83 100644 --- a/lib/microwaveprop_web/live/beacon_live/index.ex +++ b/lib/microwaveprop_web/live/beacon_live/index.ex @@ -30,7 +30,7 @@ defmodule MicrowavepropWeb.BeaconLive.Index do <:col :let={{_id, beacon}} label="Lat">{beacon.lat} <:col :let={{_id, beacon}} label="Lon">{beacon.lon} <:col :let={{_id, beacon}} label="EIRP (mW)">{Beacon.format_mw(beacon.power_mw)} - <:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft} + <:col :let={{_id, beacon}} label="Height AGL (ft)">{if beacon.height_ft, do: round(beacon.height_ft)} <:col :let={{_id, beacon}} label="Keying">{Beacon.keying_label(beacon.keying)} <:col :let={{_id, beacon}} label="On air"> @@ -69,7 +69,7 @@ defmodule MicrowavepropWeb.BeaconLive.Index do <:col :let={{_id, beacon}} label="Call">{beacon.callsign} <:col :let={{_id, beacon}} label="Grid">{beacon.grid} <:col :let={{_id, beacon}} label="EIRP (mW)">{Beacon.format_mw(beacon.power_mw)} - <:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft} + <:col :let={{_id, beacon}} label="Height AGL (ft)">{if beacon.height_ft, do: round(beacon.height_ft)} <:col :let={{_id, beacon}} label="Submitted"> {Calendar.strftime(beacon.inserted_at, "%Y-%m-%d %H:%M UTC")} diff --git a/lib/microwaveprop_web/live/beacon_live/show.ex b/lib/microwaveprop_web/live/beacon_live/show.ex index e285524a..7d9a9e01 100644 --- a/lib/microwaveprop_web/live/beacon_live/show.ex +++ b/lib/microwaveprop_web/live/beacon_live/show.ex @@ -118,7 +118,7 @@ defmodule MicrowavepropWeb.BeaconLive.Show do <.stat_field label="Longitude">{format_coord(@beacon.lon)} <.stat_field label="EIRP">{Beacon.format_mw(@beacon.power_mw)} mW - <.stat_field label="Height AGL">{Beacon.format_mw(@beacon.height_ft)} ft + <.stat_field label="Height AGL">{if @beacon.height_ft, do: round(@beacon.height_ft)} ft <.stat_field label="Bearing">{bearing_label(@beacon.bearing)} <.stat_field :if={@beacon.beamwidth_deg} label="Beamwidth"> {Beacon.format_mw(@beacon.beamwidth_deg)}° @@ -199,7 +199,7 @@ defmodule MicrowavepropWeb.BeaconLive.Show do defp bearing_label("omni"), do: "Omni" defp bearing_label(value), do: "#{value}°" - defp format_coord(value) when is_float(value), do: :erlang.float_to_binary(value, decimals: 5) + defp format_coord(value) when is_float(value), do: :erlang.float_to_binary(value, decimals: 6) defp format_coord(value), do: to_string(value) attr :label, :string, required: true