weather page
This commit is contained in:
parent
46d34a7c5e
commit
51efdf34a2
18 changed files with 173 additions and 133 deletions
|
|
@ -393,26 +393,29 @@ defmodule AprsWeb.CoreComponents do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a header with title.
|
Renders a modern site header with navigation links.
|
||||||
"""
|
"""
|
||||||
attr :class, :string, default: nil
|
attr :class, :string, default: ""
|
||||||
|
|
||||||
slot :inner_block, required: true
|
|
||||||
slot :subtitle
|
|
||||||
slot :actions
|
|
||||||
|
|
||||||
def header(assigns) do
|
def header(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
|
<header class={["bg-white shadow sticky top-0 z-40", @class]}>
|
||||||
<div>
|
<nav
|
||||||
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
|
class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 flex h-16 items-center justify-between"
|
||||||
{render_slot(@inner_block)}
|
aria-label="Main"
|
||||||
</h1>
|
>
|
||||||
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
|
<div class="flex items-center space-x-4">
|
||||||
{render_slot(@subtitle)}
|
<a href="/" class="text-xl font-bold text-blue-700 hover:text-blue-900 transition-colors">
|
||||||
</p>
|
aprs.me
|
||||||
</div>
|
</a>
|
||||||
<div class="flex-none">{render_slot(@actions)}</div>
|
</div>
|
||||||
|
<div class="flex items-center space-x-6">
|
||||||
|
<a href="/" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">Home</a>
|
||||||
|
<a href="/about" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
<.header />
|
||||||
<main class="min-h-screen bg-white">
|
<main class="min-h-screen bg-white">
|
||||||
<div>
|
<div>
|
||||||
<.flash kind={:info} title="Success!" flash={@flash} />
|
<.flash kind={:info} title="Success!" flash={@flash} />
|
||||||
|
|
|
||||||
9
lib/aprs_web/live/about_live.ex
Normal file
9
lib/aprs_web/live/about_live.ex
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule AprsWeb.AboutLive do
|
||||||
|
@moduledoc false
|
||||||
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
{:ok, assign(socket, page_title: "About aprs.me")}
|
||||||
|
end
|
||||||
|
end
|
||||||
0
lib/aprs_web/live/about_live.html.heex
Normal file
0
lib/aprs_web/live/about_live.html.heex
Normal file
|
|
@ -1,74 +1,6 @@
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<.header>
|
<.header />
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-semibold text-gray-900">
|
|
||||||
Bad APRS Packets
|
|
||||||
<span class="ml-2 text-base font-normal text-gray-500">
|
|
||||||
(Showing {length(@bad_packets)} most recent)
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
|
||||||
Packets that failed to parse or process (limited to 100 most recent)
|
|
||||||
<%= if @last_updated do %>
|
|
||||||
<span class="text-xs text-gray-400 ml-2">
|
|
||||||
Last updated: {Calendar.strftime(@last_updated, "%H:%M:%S UTC")}
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<button
|
|
||||||
phx-click="refresh"
|
|
||||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
disabled={@loading}
|
|
||||||
>
|
|
||||||
<%= if @loading do %>
|
|
||||||
<svg
|
|
||||||
class="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
class="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
>
|
|
||||||
</circle>
|
|
||||||
<path
|
|
||||||
class="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
>
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
Refreshing...
|
|
||||||
<% else %>
|
|
||||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Refresh
|
|
||||||
<% end %>
|
|
||||||
</button>
|
|
||||||
<.link navigate={~p"/packets"} class="text-sm text-blue-600 hover:text-blue-800">
|
|
||||||
View Good Packets →
|
|
||||||
</.link>
|
|
||||||
<.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800">
|
|
||||||
← Back to Map
|
|
||||||
</.link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.header>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
|
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
overlay_callsign: "",
|
overlay_callsign: "",
|
||||||
trail_duration: "1",
|
trail_duration: "1",
|
||||||
historical_hours: "1",
|
historical_hours: "1",
|
||||||
packet_age_threshold: 3600,
|
packet_age_threshold: one_hour_ago,
|
||||||
slideover_open: true,
|
slideover_open: true,
|
||||||
replay_active: false,
|
replay_active: false,
|
||||||
replay_start_time: nil,
|
replay_start_time: nil,
|
||||||
|
|
@ -1016,7 +1016,26 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
defp packet_within_time_threshold?(packet, threshold) do
|
defp packet_within_time_threshold?(packet, threshold) do
|
||||||
case packet do
|
case packet do
|
||||||
%{received_at: received_at} when not is_nil(received_at) ->
|
%{received_at: received_at} when not is_nil(received_at) ->
|
||||||
DateTime.compare(received_at, threshold) in [:gt, :eq]
|
threshold_dt =
|
||||||
|
cond do
|
||||||
|
is_integer(threshold) ->
|
||||||
|
# Assume seconds since epoch
|
||||||
|
DateTime.from_unix!(threshold)
|
||||||
|
|
||||||
|
is_binary(threshold) ->
|
||||||
|
case DateTime.from_iso8601(threshold) do
|
||||||
|
{:ok, dt, _} -> dt
|
||||||
|
_ -> DateTime.utc_now()
|
||||||
|
end
|
||||||
|
|
||||||
|
match?(%DateTime{}, threshold) ->
|
||||||
|
threshold
|
||||||
|
|
||||||
|
true ->
|
||||||
|
DateTime.utc_now()
|
||||||
|
end
|
||||||
|
|
||||||
|
DateTime.compare(received_at, threshold_dt) in [:gt, :eq]
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
# If no timestamp, treat as current
|
# If no timestamp, treat as current
|
||||||
|
|
@ -1381,7 +1400,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
<div class="aprs-callsign"><strong><a href="/map/callsign/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
|
<div class="aprs-callsign"><strong><a href="/map/callsign/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
|
||||||
#{comment_html}
|
#{comment_html}
|
||||||
<div class="aprs-coords">#{Float.round(PacketUtils.to_float(lat), 4)}, #{Float.round(PacketUtils.to_float(lon), 4)}</div>
|
<div class="aprs-coords">#{Float.round(PacketUtils.to_float(lat), 4)}, #{Float.round(PacketUtils.to_float(lon), 4)}</div>
|
||||||
<div class="aprs-time">#{packet_info.timestamp}</div>
|
<div class="aprs-time">#{format_popup_timestamp(packet_info.timestamp)}</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
@ -1408,11 +1427,11 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
defp build_weather_popup_html(packet, callsign) do
|
defp build_weather_popup_html(packet, callsign) do
|
||||||
received_at = get_received_at(packet)
|
received_at = get_received_at(packet)
|
||||||
timestamp_str = format_timestamp(received_at)
|
timestamp_str = format_popup_timestamp(received_at)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
<strong>#{callsign} - Weather Report</strong><br>
|
<strong>#{callsign} - Weather Report</strong><br>
|
||||||
<small>#{timestamp_str} UTC</small>
|
<small>#{timestamp_str}</small>
|
||||||
<hr>
|
<hr>
|
||||||
Temperature: #{PacketUtils.get_weather_field(packet, :temperature)}°F<br>
|
Temperature: #{PacketUtils.get_weather_field(packet, :temperature)}°F<br>
|
||||||
Humidity: #{PacketUtils.get_weather_field(packet, :humidity)}%<br>
|
Humidity: #{PacketUtils.get_weather_field(packet, :humidity)}%<br>
|
||||||
|
|
@ -1432,10 +1451,30 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp format_timestamp(received_at) do
|
defp format_popup_timestamp(ts) do
|
||||||
if received_at,
|
cond do
|
||||||
do: Calendar.strftime(received_at, "%Y-%m-%d %H:%M:%S"),
|
is_binary(ts) ->
|
||||||
else: "N/A"
|
case DateTime.from_iso8601(ts) do
|
||||||
|
{:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
_ -> ts
|
||||||
|
end
|
||||||
|
|
||||||
|
is_integer(ts) ->
|
||||||
|
ts
|
||||||
|
|> DateTime.from_unix!(:millisecond)
|
||||||
|
|> Calendar.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
match?(%DateTime{}, ts) ->
|
||||||
|
Calendar.strftime(ts, "%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
match?(%NaiveDateTime{}, ts) ->
|
||||||
|
ts
|
||||||
|
|> DateTime.from_naive!("Etc/UTC")
|
||||||
|
|> Calendar.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
true ->
|
||||||
|
to_string(ts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec generate_callsign(map() | struct()) :: String.t()
|
@spec generate_callsign(map() | struct()) :: String.t()
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,6 @@
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<.header>
|
<.header />
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-semibold text-gray-900">Packets for {@callsign}</h1>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
|
||||||
Showing up to 100 packets (stored and live) for callsign {@callsign}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800">
|
|
||||||
← Back to Map
|
|
||||||
</.link>
|
|
||||||
<.link navigate={~p"/packets"} class="text-sm text-blue-600 hover:text-blue-800">
|
|
||||||
All Packets
|
|
||||||
</.link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.header>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if @error do %>
|
<%= if @error do %>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,6 @@
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<.header>
|
<.header />
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-semibold text-gray-900">APRS Packets</h1>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">Live and recent APRS packets from the network</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<.link navigate={~p"/badpackets"} class="text-sm text-blue-600 hover:text-blue-800">
|
|
||||||
View Bad Packets →
|
|
||||||
</.link>
|
|
||||||
<.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800">
|
|
||||||
← Back to Map
|
|
||||||
</.link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.header>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
|
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ defmodule AprsWeb.UserConfirmationInstructionsLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.header>Resend confirmation instructions</.header>
|
<.header />
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
:let={f}
|
:let={f}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule AprsWeb.UserConfirmationLive do
|
||||||
def render(%{live_action: :edit} = assigns) do
|
def render(%{live_action: :edit} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="mx-auto max-w-sm">
|
<div class="mx-auto max-w-sm">
|
||||||
<.header class="text-center">Confirm Account</.header>
|
<.header class="text-center" />
|
||||||
|
|
||||||
<.simple_form :let={f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account">
|
<.simple_form :let={f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account">
|
||||||
<.input field={{f, :token}} type="hidden" value={@token} />
|
<.input field={{f, :token}} type="hidden" value={@token} />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ defmodule AprsWeb.UserForgotPasswordLive do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="mx-auto max-w-sm">
|
<div class="mx-auto max-w-sm">
|
||||||
|
<.header class="text-center" />
|
||||||
|
|
||||||
<.header class="text-center">
|
<.header class="text-center">
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
<:subtitle>We'll send a password reset link to your inbox</:subtitle>
|
<:subtitle>We'll send a password reset link to your inbox</:subtitle>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ defmodule AprsWeb.UserLoginLive do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="mx-auto max-w-sm">
|
<div class="mx-auto max-w-sm">
|
||||||
|
<.header class="text-center" />
|
||||||
|
|
||||||
<.header class="text-center">
|
<.header class="text-center">
|
||||||
Sign in to account
|
Sign in to account
|
||||||
<:subtitle>
|
<:subtitle>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ defmodule AprsWeb.UserRegistrationLive do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="mx-auto max-w-sm">
|
<div class="mx-auto max-w-sm">
|
||||||
|
<.header class="text-center" />
|
||||||
|
|
||||||
<.header class="text-center">
|
<.header class="text-center">
|
||||||
Register for an account
|
Register for an account
|
||||||
<:subtitle>
|
<:subtitle>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule AprsWeb.UserResetPasswordLive do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="mx-auto max-w-sm">
|
<div class="mx-auto max-w-sm">
|
||||||
<.header class="text-center">Reset Password</.header>
|
<.header class="text-center" />
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
:let={f}
|
:let={f}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ defmodule AprsWeb.UserSettingsLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.header>Change Email</.header>
|
<.header />
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
:let={f}
|
:let={f}
|
||||||
|
|
@ -35,7 +35,7 @@ defmodule AprsWeb.UserSettingsLive do
|
||||||
</:actions>
|
</:actions>
|
||||||
</.simple_form>
|
</.simple_form>
|
||||||
|
|
||||||
<.header>Change Password</.header>
|
<.header />
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
:let={f}
|
:let={f}
|
||||||
|
|
|
||||||
27
lib/aprs_web/live/weather_live/callsign_view.ex
Normal file
27
lib/aprs_web/live/weather_live/callsign_view.ex
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule AprsWeb.WeatherLive.CallsignView do
|
||||||
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
|
alias Aprs.Packets
|
||||||
|
alias AprsWeb.MapLive.PacketUtils
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||||
|
normalized_callsign = String.upcase(String.trim(callsign))
|
||||||
|
weather_packet = get_latest_weather_packet(normalized_callsign)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:callsign, normalized_callsign)
|
||||||
|
|> assign(:weather_packet, weather_packet)
|
||||||
|
|> assign(:page_title, "Weather for #{normalized_callsign}")
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_latest_weather_packet(callsign) do
|
||||||
|
# Get the most recent packet for this callsign that is a weather report
|
||||||
|
%{callsign: callsign, limit: 10}
|
||||||
|
|> Packets.get_recent_packets()
|
||||||
|
|> Enum.find(&PacketUtils.is_weather_packet?/1)
|
||||||
|
end
|
||||||
|
end
|
||||||
53
lib/aprs_web/live/weather_live/callsign_view.html.heex
Normal file
53
lib/aprs_web/live/weather_live/callsign_view.html.heex
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<%= if @weather_packet do %>
|
||||||
|
<div class="max-w-lg mx-auto mt-8 p-6 bg-white rounded shadow">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
|
||||||
|
<div class="text-gray-600 mb-4">
|
||||||
|
{PacketUtils.get_timestamp(@weather_packet)}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<strong>Temperature:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :temperature)}°F
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Humidity:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Wind:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}° @ {PacketUtils.get_weather_field(
|
||||||
|
@weather_packet,
|
||||||
|
:wind_speed
|
||||||
|
)} mph
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Gusts:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :wind_gust)} mph
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Pressure:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Rain (1h):</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :rain_1h)} in
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Rain (24h):</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :rain_24h)} in
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Rain (since midnight):</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :rain_since_midnight)} in
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-sm text-gray-500">
|
||||||
|
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="max-w-lg mx-auto mt-8 p-6 bg-white rounded shadow text-center">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
|
||||||
|
<div class="text-gray-600">No recent weather data available for this callsign.</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
@ -40,6 +40,8 @@ defmodule AprsWeb.Router do
|
||||||
live "/packets", PacketsLive.Index, :index
|
live "/packets", PacketsLive.Index, :index
|
||||||
live "/packets/:callsign", PacketsLive.CallsignView, :index
|
live "/packets/:callsign", PacketsLive.CallsignView, :index
|
||||||
live "/badpackets", BadPacketsLive.Index, :index
|
live "/badpackets", BadPacketsLive.Index, :index
|
||||||
|
live "/weather/:callsign", WeatherLive.CallsignView, :index
|
||||||
|
live "/about", AboutLive, :index
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue