aprs.me/lib/aprsme_web/live/info_live/show.html.heex
Graham McIntire 993af8447e
Move APRS icons before callsigns on info page
Reorder icon and callsign in the page header, Other SSIDs table, and
Stations Near Current Position table so the icon appears first. Fix
icon clipping with flex-shrink-0 h-8 and add py-2 padding to SSIDs
table cells for proper vertical alignment.
2026-02-19 18:55:11 -06:00

743 lines
35 KiB
Text

<!-- Leaflet is already loaded via vendor bundles in the layout -->
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- Page header -->
<div class="bg-white shadow-sm dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-4 md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1">
<div class="flex items-center">
<h1 class="text-xl font-bold leading-7 sm:truncate sm:text-2xl">
{gettext("APRS Station")}
</h1>
<div class="ml-3 flex items-center space-x-3">
<%= if @packet do %>
<% {symbol_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(@packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div title={display_symbol}>
{render_symbol_html(@packet)}
</div>
<% end %>
<span class="inline-flex items-center rounded-md bg-indigo-100 px-2 py-1 text-xs font-medium text-indigo-700 dark:bg-indigo-400/10 dark:text-indigo-400">
{@callsign}
</span>
</div>
</div>
</div>
<div class="mt-3 flex md:ml-4 md:mt-0">
<div class="flex space-x-2">
<.link
navigate={~p"/packets/#{@callsign}"}
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
{gettext("View packets")}
</.link>
<%= if @has_weather_packets do %>
<.link
navigate={~p"/weather/#{@callsign}"}
class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{gettext("Weather charts")}
</.link>
<% end %>
</div>
</div>
</div>
</div>
</div>
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<%= if @packet do %>
<!-- Station details -->
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mb-6 lg:grid-rows-1">
<!-- Position information -->
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10 h-full">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Position Information")}</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Location")}</dt>
<dd class="mt-1 text-sm font-semibold">
{@packet.lat || ""}, {@packet.lon || ""}
</dd>
</div>
<%= if @packet.lat && @packet.lon do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Grid Square")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= case Gridsquare.encode(@packet.lon, @packet.lat) do %>
<% %Gridsquare.EncodeResult{grid_reference: grid_ref} -> %>
{grid_ref}
<% _ -> %>
-
<% end %>
</dd>
</div>
<% end %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Last Activity")}</dt>
<dd class="mt-1 text-sm">
<%= if @packet.received_at do %>
<div
class="font-semibold"
phx-hook="TimeAgoHook"
id="last-position-time"
data-timestamp={DateTime.to_iso8601(@packet.received_at)}
>
{AprsmeWeb.TimeHelpers.time_ago_in_words(@packet.received_at)}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{Calendar.strftime(@packet.received_at, "%Y-%m-%d %H:%M:%S UTC")}
</div>
<% else %>
<span class="text-gray-500 dark:text-gray-400">{gettext("Unknown")}</span>
<% end %>
</dd>
</div>
<%= if @packet.altitude do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Altitude")}</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.altitude} ft</dd>
</div>
<% end %>
<%= if @packet.course do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Course")}</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.course}°</dd>
</div>
<% end %>
<%= if @packet.speed do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Speed")}</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.speed} MPH</dd>
</div>
<% end %>
<%= if @packet.comment do %>
<div class="col-span-2">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Comment")}</dt>
<dd class="mt-1 text-sm">
<%= if is_binary(@packet.comment) do %>
{Aprsme.EncodingUtils.sanitize_string(@packet.comment)}
<% else %>
{@packet.comment}
<% end %>
</dd>
</div>
<% end %>
<%= if @packet.phg_power || @packet.phg_height || @packet.phg_gain do %>
<div class="col-span-2">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
{gettext("PHG (Power-Height-Gain)")}
</dt>
<dd class="grid grid-cols-2 gap-2 text-sm">
<%= if @packet.phg_power do %>
<div>
<span class="text-gray-500 dark:text-gray-400">{gettext("Power:")}</span>
<span class="font-semibold">{@packet.phg_power} W</span>
</div>
<% end %>
<%= if @packet.phg_height do %>
<div>
<span class="text-gray-500 dark:text-gray-400">{gettext("HAAT:")}</span>
<span class="font-semibold">{@packet.phg_height} ft</span>
</div>
<% end %>
<%= if @packet.phg_gain do %>
<div>
<span class="text-gray-500 dark:text-gray-400">{gettext("Gain:")}</span>
<span class="font-semibold">{@packet.phg_gain} dBi</span>
</div>
<% end %>
<%= if @packet.phg_directivity do %>
<div>
<span class="text-gray-500 dark:text-gray-400">{gettext("Dir:")}</span>
<span class="font-semibold">
<%= if @packet.phg_directivity == 0 or @packet.phg_directivity == 360 do %>
{gettext("Omni")}
<% else %>
{@packet.phg_directivity}°
<% end %>
</span>
</div>
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<!-- Map showing station location -->
<%= if @packet && @packet.lat && @packet.lon do %>
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10 h-full">
<% {symbol_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(@packet) %>
<% symbol_html = AprsmeWeb.AprsSymbol.render_marker_html(symbol_table, symbol_code, @callsign, 32) %>
<.info_map
id="station-location-map"
lat={Aprsme.EncodingUtils.to_float(@packet.lat)}
lon={Aprsme.EncodingUtils.to_float(@packet.lon)}
callsign={@callsign}
symbol_html={symbol_html}
height="100%"
zoom={12}
/>
</div>
<% end %>
<!-- Row for Device Information and Other SSIDs -->
<div class="lg:col-span-2 grid grid-cols-1 gap-4 lg:grid-cols-2">
<!-- Device information -->
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0020.75 3H3.75A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Device Information")}</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Device")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if @packet.device_model || @packet.device_vendor do %>
{[
@packet.device_model,
@packet.device_vendor,
@packet.device_contact
]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")}
<%= if @packet.device_class do %>
({@packet.device_class})
<% end %>
<% else %>
<span class="text-gray-500 dark:text-gray-400 font-mono">
{@packet.device_identifier || gettext("Unknown")}
</span>
<% end %>
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Path")}</dt>
<dd class="mt-1 text-sm font-semibold">
<% path_elements = parse_path_with_links(@packet.path) %>
<%= for {{type, content, display}, index} <- Enum.with_index(path_elements) do %>
<%= case type do %>
<% :link -> %>
<.link
navigate={~p"/info/#{content}"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{display}
</.link>
<% :text -> %>
{content}
<% end %>
<%= if index < length(path_elements) - 1 do %>
<span class="text-gray-400 dark:text-gray-500">,</span>
<% end %>
<% end %>
</dd>
</div>
<%= if @packet.path && @packet.path != "" do %>
<div class="col-span-2">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Decoded Path")}</dt>
<dd class="mt-1 text-sm">
{decode_aprs_path(@packet.path)}
</dd>
</div>
<% end %>
<div class="col-span-2">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Raw Packet")}</dt>
<dd class="mt-1 text-xs font-mono break-all">
<%= if is_binary(@packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
<% else %>
{@packet.raw_packet || ""}
<% end %>
</dd>
</div>
</dl>
</div>
</div>
<!-- Other SSIDs -->
<%= if length(@other_ssids) > 0 do %>
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Other SSIDs")}</h3>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700 text-sm">
<thead>
<tr>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Callsign")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Distance & Direction")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Last Heard")}
</th>
</tr>
</thead>
<tbody>
<%= for ssid_info <- @other_ssids do %>
<tr>
<td class="font-medium py-2">
<div class="flex items-center space-x-2">
<%= if ssid_info.packet do %>
<% {symbol_table, symbol_code} =
AprsmeWeb.AprsSymbol.extract_from_packet(ssid_info.packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div class="flex-shrink-0 h-8" title={display_symbol}>
{render_symbol_html(ssid_info.packet)}
</div>
<% end %>
<.link
navigate={~p"/info/#{ssid_info.callsign}"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{ssid_info.callsign}
</.link>
</div>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if ssid_info.packet && ssid_info.packet.lat && ssid_info.packet.lon && @packet && @packet.lat && @packet.lon do %>
<% dist =
haversine(
@packet.lat,
@packet.lon,
ssid_info.packet.lat,
ssid_info.packet.lon
) %>
<% course =
calculate_course(
@packet.lat,
@packet.lon,
ssid_info.packet.lat,
ssid_info.packet.lon
) %>
<% locale = Map.get(assigns, :locale, "en") %>
{format_distance(dist, locale)} @ {Float.round(course, 0)}°
<% else %>
-
<% end %>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if ssid_info.received_at do %>
<span
class="text-sm"
phx-hook="TimeAgoHook"
id={"ssid-time-#{ssid_info.ssid}"}
data-timestamp={DateTime.to_iso8601(ssid_info.received_at)}
>
{AprsmeWeb.TimeHelpers.time_ago_in_words(ssid_info.received_at)}
</span>
<% else %>
<span class="text-sm text-gray-500 dark:text-gray-400">{gettext("Unknown")}</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% else %>
<!-- Empty placeholder to maintain grid layout -->
<div></div>
<% end %>
</div>
</div>
<!-- Neighboring stations -->
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Stations Near Current Position")}</h3>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead>
<tr>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Callsign")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Distance & Course")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Last Heard")}
</th>
</tr>
</thead>
<tbody>
<%= for neighbor <- @neighbors do %>
<tr>
<td class="font-medium">
<div class="flex items-center space-x-2">
<%= if neighbor.packet do %>
<% {symbol_table, symbol_code} =
AprsmeWeb.AprsSymbol.extract_from_packet(neighbor.packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div class="flex-shrink-0 h-8" title={display_symbol}>
{render_symbol_html(neighbor.packet)}
</div>
<% end %>
<.link
navigate={~p"/info/#{neighbor.callsign}"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{neighbor.callsign}
</.link>
</div>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if neighbor.course do %>
{neighbor.distance || ""} @ {Float.round(neighbor.course, 0)}°
<% else %>
{neighbor.distance || ""}
<% end %>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if neighbor.last_heard do %>
<div
class="font-semibold"
phx-hook="TimeAgoHook"
id={"neighbor-time-#{neighbor.callsign}"}
data-timestamp={neighbor.last_heard.timestamp}
>
{neighbor.last_heard.time_ago}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{neighbor.last_heard.formatted}
</div>
<% else %>
<span class="text-gray-500 dark:text-gray-400">{gettext("Unknown")}</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<!-- Stations that heard this callsign on RF -->
<%= if length(@heard_by_stations) > 0 do %>
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10 mt-6">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">
{gettext("Stations which heard %{callsign} directly on radio",
callsign: @callsign
)}
</h3>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead>
<tr>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Callsign")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("First Heard")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Last Heard")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Packets")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Longest Path")}
</th>
</tr>
</thead>
<tbody>
<%= for station <- @heard_by_stations do %>
<tr>
<td class="font-medium">
<.link
navigate={~p"/info/#{station.digipeater}"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{station.digipeater}
</.link>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if station.first_heard do %>
<div class="text-xs font-mono">
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
</div>
<% else %>
-
<% end %>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if station.last_heard do %>
<div class="font-semibold">
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
</div>
<% else %>
-
<% end %>
</td>
<td>
{station.packet_count}
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if station.longest_distance do %>
{station.longest_distance}
<%= if station.longest_path_time do %>
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
</div>
<% end %>
<% else %>
-
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% end %>
<!-- Stations heard directly by this callsign -->
<%= if length(@stations_heard_by) > 0 do %>
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10 mt-6">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">
{gettext("Stations heard directly by %{callsign}",
callsign: @callsign
)}
</h3>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead>
<tr>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Callsign")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("First Heard")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Last Heard")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Packets")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Longest Path")}
</th>
</tr>
</thead>
<tbody>
<%= for station <- @stations_heard_by do %>
<tr>
<td class="font-medium">
<.link
navigate={~p"/info/#{station.station}"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{station.station}
</.link>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if station.first_heard do %>
<div class="text-xs font-mono">
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
</div>
<% else %>
-
<% end %>
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if station.last_heard do %>
<div class="font-semibold">
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
</div>
<% else %>
-
<% end %>
</td>
<td>
{station.packet_count}
</td>
<td class="text-gray-500 dark:text-gray-400">
<%= if station.longest_distance do %>
{station.longest_distance}
<%= if station.longest_path_time do %>
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
</div>
<% end %>
<% else %>
-
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% end %>
<% else %>
<div class="text-center py-8">
<svg
class="mx-auto h-8 w-8 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423L16.5 15.75l.394 1.183a2.25 2.25 0 001.423 1.423L19.5 18.75l-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
<h3 class="mt-2 text-sm font-semibold">{gettext("No data available")}</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{gettext("No recent packet data available for this callsign.")}
</p>
</div>
<% end %>
</div>
</div>