Apply TailwindCSS Plus design patterns across site

Dark navbar header with radio icon, refined slideover control panel
with branded header and section cards, polished cards/tables/badges
on info, packets, and about pages using ring borders and shadow-xs.
This commit is contained in:
Graham McIntire 2026-02-20 17:56:00 -06:00
parent ab356afe43
commit 0061c4b169
No known key found for this signature in database
6 changed files with 458 additions and 375 deletions

View file

@ -433,22 +433,30 @@ defmodule AprsmeWeb.CoreComponents do
def header(assigns) do
~H"""
<header class="site-header bg-white shadow-sm dark:bg-gray-800/50 dark:shadow-none dark:border-b dark:border-white/10">
<header class="site-header relative bg-gray-800 dark:bg-gray-800/50 dark:after:pointer-events-none dark:after:absolute dark:after:inset-x-0 dark:after:bottom-0 dark:after:h-px dark:after:bg-white/10">
<nav class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
<div class="flex items-center">
<.link
navigate="/"
class="text-xl font-bold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
class="flex items-center gap-2 text-xl font-bold text-white hover:text-gray-200"
>
<svg class="h-6 w-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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>
aprs.me
<%= if @dev_mode do %>
<span class="ml-2 inline-flex items-center rounded-md bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-800 dark:bg-yellow-400/10 dark:text-yellow-500">
<span class="ml-1 inline-flex items-center rounded-md bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-500 ring-1 ring-inset ring-yellow-400/20">
DEV
</span>
<% end %>
</.link>
</div>
<div class="hidden lg:flex lg:items-center lg:gap-x-8">
<div class="hidden lg:flex lg:items-center lg:gap-x-1">
<.navigation variant={:horizontal} current_user={@current_user} />
</div>
<div class="flex items-center gap-2">
@ -458,15 +466,20 @@ defmodule AprsmeWeb.CoreComponents do
phx-click={
JS.toggle(to: "#mobile-menu", in: "transition ease-out duration-100", out: "transition ease-in duration-75")
}
class="rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white"
class="relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-white/5 hover:text-white focus:outline-2 focus:-outline-offset-1 focus:outline-indigo-500"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</button>
<div
id="mobile-menu"
class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-gray-800 py-2 shadow-lg ring-1 ring-white/10"
>
<.navigation variant={:vertical} current_user={@current_user} />
</div>
@ -487,7 +500,7 @@ defmodule AprsmeWeb.CoreComponents do
phx-click={
JS.toggle(to: "#theme-menu", in: "transition ease-out duration-100", out: "transition ease-in duration-75")
}
class="rounded-full p-2 text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-white"
class="relative rounded-full p-1.5 text-gray-400 hover:text-white focus:outline-2 focus:outline-offset-2 focus:outline-indigo-500"
>
<svg class="h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z" />
@ -496,11 +509,11 @@ defmodule AprsmeWeb.CoreComponents do
<div
id="theme-menu"
phx-click-away={JS.hide(to: "#theme-menu")}
class="hidden absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
class="hidden absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-gray-800 py-1 shadow-lg ring-1 ring-white/10"
>
<button
phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "light"}) |> JS.hide(to: "#theme-menu")}
class="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-200 hover:bg-white/5"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path
@ -513,7 +526,7 @@ defmodule AprsmeWeb.CoreComponents do
</button>
<button
phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "dark"}) |> JS.hide(to: "#theme-menu")}
class="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-200 hover:bg-white/5"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
@ -522,7 +535,7 @@ defmodule AprsmeWeb.CoreComponents do
</button>
<button
phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "auto"}) |> JS.hide(to: "#theme-menu")}
class="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-200 hover:bg-white/5"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path
@ -749,7 +762,7 @@ defmodule AprsmeWeb.CoreComponents do
Renders a navigation menu component that can be used in both regular pages and map sidebars.
"""
attr :class, :string, default: ""
attr :variant, :atom, values: [:horizontal, :vertical], default: :horizontal
attr :variant, :atom, values: [:horizontal, :vertical, :sidebar], default: :horizontal
attr :current_user, :any, default: nil
attr :map_state, :map, default: nil
attr :tracked_callsign, :string, default: ""
@ -775,93 +788,142 @@ defmodule AprsmeWeb.CoreComponents do
<%= if @variant == :horizontal do %>
<.link
navigate={@home_url}
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Home")}
</.link>
<.link
navigate="/api"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
API
</.link>
<.link
navigate="/about"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("About")}
</.link>
<%= if @current_user do %>
<.link
navigate="/users/settings"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Settings")}
</.link>
<.link
href="/users/log_out"
method="delete"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Log out")}
</.link>
<% else %>
<.link
navigate="/users/register"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Register")}
</.link>
<.link
navigate="/users/log_in"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
class="rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-400"
>
{gettext("Log in")}
</.link>
<% end %>
<% else %>
<% end %>
<%= if @variant == :vertical do %>
<.link
navigate={@home_url}
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Home")}
</.link>
<.link
navigate="/api"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
API
</.link>
<.link
navigate="/about"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("About")}
</.link>
<%= if @current_user do %>
<.link
navigate="/users/settings"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Settings")}
</.link>
<.link
href="/users/log_out"
method="delete"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Log out")}
</.link>
<% else %>
<.link
navigate="/users/register"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-white/5 hover:text-white"
>
{gettext("Register")}
</.link>
<.link
navigate="/users/log_in"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
class="block rounded-md bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-400"
>
{gettext("Log in")}
</.link>
<% end %>
<% end %>
<%= if @variant == :sidebar do %>
<.link
navigate={@home_url}
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
>
{gettext("Home")}
</.link>
<.link
navigate="/api"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
>
API
</.link>
<.link
navigate="/about"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
>
{gettext("About")}
</.link>
<%= if @current_user do %>
<.link
navigate="/users/settings"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
>
{gettext("Settings")}
</.link>
<.link
href="/users/log_out"
method="delete"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
>
{gettext("Log out")}
</.link>
<% else %>
<.link
navigate="/users/register"
class="block rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
>
{gettext("Register")}
</.link>
<.link
navigate="/users/log_in"
class="block rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500"
>
{gettext("Log in")}
</.link>

View file

@ -1,40 +1,62 @@
<div class="mx-auto max-w-2xl py-12 px-4 sm:px-6 lg:px-8">
<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 mb-8">
<div class="px-6 py-8 prose max-w-none dark:prose-invert">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">{gettext("Under heavy construction")}</h2>
<p class="text-gray-600 dark:text-gray-300">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</p>
<p class="text-gray-600 dark:text-gray-300">
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
</p>
<div class="bg-gray-50 p-6 rounded-lg mb-8 dark:bg-gray-700/50">
<h4 class="text-lg font-semibold mb-3 text-gray-900 dark:text-white">
<div class="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10 mb-8">
<div class="px-6 py-8 sm:px-8">
<div class="flex items-center gap-3 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-600">
<svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">APRS.me</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">{gettext("Real-time APRS packet tracking")}</p>
</div>
</div>
<div class="prose max-w-none dark:prose-invert">
<p class="text-gray-600 dark:text-gray-300">
APRS.me is a real-time Automatic Packet Reporting System (APRS) viewer built for amateur radio operators. Track stations, view position reports, weather data, and telemetry from the APRS-IS network on an interactive map.
</p>
</div>
<div class="mt-8 rounded-lg bg-gray-50 p-5 ring-1 ring-gray-950/5 dark:bg-gray-900/50 dark:ring-white/10">
<h4 class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
{gettext("Built with Modern Technologies")}
</h4>
<div class="flex flex-wrap gap-2">
<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">
<span class="inline-flex items-center rounded-md bg-indigo-50 px-2.5 py-1 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-700/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:ring-indigo-400/30">
Elixir
</span>
<span class="inline-flex items-center rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 dark:bg-green-400/10 dark:text-green-400">
<span class="inline-flex items-center rounded-md bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-700/10 dark:bg-green-400/10 dark:text-green-400 dark:ring-green-400/30">
Phoenix
</span>
<span class="inline-flex items-center rounded-md bg-purple-100 px-2 py-1 text-xs font-medium text-purple-700 dark:bg-purple-400/10 dark:text-purple-400">
<span class="inline-flex items-center rounded-md bg-purple-50 px-2.5 py-1 text-xs font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10 dark:bg-purple-400/10 dark:text-purple-400 dark:ring-purple-400/30">
LiveView
</span>
<span class="inline-flex items-center rounded-md bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-800 dark:bg-yellow-400/10 dark:text-yellow-500">
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2.5 py-1 text-xs font-medium text-yellow-800 ring-1 ring-inset ring-yellow-700/10 dark:bg-yellow-400/10 dark:text-yellow-500 dark:ring-yellow-400/30">
PostgreSQL
</span>
<span class="inline-flex items-center rounded-md bg-red-100 px-2 py-1 text-xs font-medium text-red-700 dark:bg-red-400/10 dark:text-red-400">
<span class="inline-flex items-center rounded-md bg-cyan-50 px-2.5 py-1 text-xs font-medium text-cyan-700 ring-1 ring-inset ring-cyan-700/10 dark:bg-cyan-400/10 dark:text-cyan-400 dark:ring-cyan-400/30">
PostGIS
</span>
<span class="inline-flex items-center rounded-md bg-red-50 px-2.5 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-700/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/30">
Tailwind CSS
</span>
<span class="inline-flex items-center rounded-md bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30">
Leaflet.js
</span>
</div>
</div>
</div>
</div>
<div class="text-center text-gray-500 dark:text-gray-400">
<p class="text-sm">
&copy; 2026 aprs.me.
<div class="text-center text-gray-400 dark:text-gray-500">
<p class="text-xs">
&copy; 2026 aprs.me
</p>
</div>
</div>

View file

@ -2,9 +2,9 @@
<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="bg-white shadow-xs ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-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="py-5 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">
@ -22,7 +22,7 @@
{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">
<span class="inline-flex items-center rounded-md bg-indigo-50 px-2.5 py-1 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-700/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:ring-indigo-400/30">
{@callsign}
</span>
</div>
@ -32,7 +32,7 @@
<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"
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
{gettext("View packets")}
</.link>
@ -55,7 +55,7 @@
<!-- 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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10 h-full">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
@ -188,7 +188,7 @@
<!-- 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">
<div class="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-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
@ -206,7 +206,7 @@
<!-- 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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
@ -296,7 +296,7 @@
<!-- 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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
@ -319,7 +319,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700 text-sm">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10 text-sm">
<thead>
<tr>
<th class="text-left text-xs font-medium uppercase tracking-wider">
@ -409,7 +409,7 @@
</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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
@ -432,7 +432,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr>
<th class="text-left text-xs font-medium uppercase tracking-wider">
@ -505,7 +505,7 @@
<!-- 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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10 mt-6">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
@ -532,7 +532,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr>
<th class="text-left text-xs font-medium uppercase tracking-wider">
@ -610,7 +610,7 @@
<!-- 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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10 mt-6">
<div class="px-6 py-6">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
@ -637,7 +637,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr>
<th class="text-left text-xs font-medium uppercase tracking-wider">

View file

@ -1404,294 +1404,265 @@ defmodule AprsmeWeb.MapLive.Index do
]}
>
<!-- Header -->
<div class="flex items-center justify-between p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-indigo-600 to-purple-600 text-white">
<div class="flex items-center space-x-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
<h2 class="text-xl font-bold">APRS.me</h2>
</div>
<!-- Close button for mobile -->
<button
class="lg:hidden text-white hover:text-slate-200 transition-colors"
phx-click={toggle_slideover_js()}
title={Gettext.gettext(AprsmeWeb.Gettext, "Close controls")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m18 6-12 12" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<!-- Content -->
<div class="p-6 space-y-6 bg-slate-50 dark:bg-slate-900 flex-1 overflow-y-auto">
<!-- Callsign Search -->
<div class="space-y-4">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 flex items-center space-x-2">
<svg class="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="bg-indigo-700 px-5 py-5 dark:bg-indigo-800">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2.5">
<svg class="h-5 w-5 text-indigo-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
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>
<span>{gettext("Search Callsign")}</span>
</label>
<form phx-submit="track_callsign" class="flex flex-col space-y-2">
<div class="flex space-x-2 w-full">
<input
type="text"
name="callsign"
value={@tracked_callsign || @overlay_callsign}
phx-change="update_callsign"
placeholder={gettext("Enter callsign...")}
class="flex-1 px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm uppercase placeholder-slate-400 dark:placeholder-slate-500 transition-all duration-200 hover:border-slate-400 dark:hover:border-slate-500 bg-white dark:bg-slate-800 text-gray-900 dark:text-white"
/>
<button
type="submit"
class="px-4 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-xl hover:from-indigo-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-all duration-200 text-sm font-semibold shadow-lg hover:shadow-xl whitespace-nowrap flex-shrink-0"
>
{gettext("Track")}
</button>
</div>
<%= if @tracked_callsign != "" do %>
<div class="flex items-center justify-between bg-blue-50 px-3 py-2 rounded-lg">
<span class="text-sm text-blue-700 font-medium">
{gettext("Tracking:")} <span class="font-bold">{@tracked_callsign}</span>
</span>
<button
type="button"
phx-click="clear_tracking"
class="text-blue-600 hover:text-blue-800 font-medium text-sm"
>
{gettext("Clear")}
</button>
</div>
<% end %>
</form>
<h2 class="text-base font-semibold text-white">APRS.me</h2>
</div>
<button
class="relative rounded-md text-indigo-200 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white lg:hidden"
phx-click={toggle_slideover_js()}
title={Gettext.gettext(AprsmeWeb.Gettext, "Close controls")}
>
<span class="absolute -inset-2.5"></span>
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M6 18 18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
<!-- Other SSIDs -->
<%= if @tracked_callsign != "" and @other_ssids != [] do %>
<div class="space-y-3">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 flex items-center space-x-2">
<svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<p class="mt-1 text-sm text-indigo-300 dark:text-indigo-200">
{gettext("Real-time APRS packet tracking")}
</p>
</div>
<!-- Content -->
<div class="relative flex-1 overflow-y-auto bg-white px-5 py-5 dark:bg-gray-800 dark:after:absolute dark:after:inset-y-0 dark:after:left-0 dark:after:w-px dark:after:bg-white/10">
<div class="space-y-5">
<!-- Callsign Search -->
<div class="rounded-lg bg-gray-50 p-4 ring-1 ring-gray-950/5 dark:bg-gray-900/50 dark:ring-white/10">
<label class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
<svg class="h-3.5 w-3.5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<span>{gettext("Other SSIDs")}</span>
{gettext("Search Callsign")}
</label>
<div class="space-y-1">
<%= for ssid_info <- @other_ssids do %>
<div class="flex items-center justify-between px-3 py-2 rounded-lg bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:border-indigo-300 dark:hover:border-indigo-600 transition-all duration-200 text-sm group">
<form phx-submit="track_callsign" class="space-y-2">
<div class="flex gap-2">
<input
type="text"
name="callsign"
value={@tracked_callsign || @overlay_callsign}
phx-change="update_callsign"
placeholder={gettext("Enter callsign...")}
class="block w-full rounded-md bg-white px-3 py-2 text-sm text-gray-900 shadow-xs outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 uppercase dark:bg-gray-800 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
/>
<button
type="submit"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 whitespace-nowrap flex-shrink-0 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{gettext("Track")}
</button>
</div>
<%= if @tracked_callsign != "" do %>
<div class="flex items-center justify-between rounded-md bg-indigo-50 px-3 py-2 dark:bg-indigo-500/10">
<span class="text-sm text-indigo-700 font-medium dark:text-indigo-300">
{gettext("Tracking:")} <span class="font-bold font-mono">{@tracked_callsign}</span>
</span>
<button
phx-click="track_callsign"
phx-value-callsign={ssid_info.callsign}
class="font-semibold text-indigo-600 dark:text-indigo-400 group-hover:text-indigo-800 dark:group-hover:text-indigo-300 cursor-pointer"
type="button"
phx-click="clear_tracking"
class="rounded-md bg-indigo-50 px-2 py-1 text-xs font-semibold text-indigo-600 hover:bg-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-400 dark:hover:bg-indigo-500/20"
>
{ssid_info.callsign}
{gettext("Clear")}
</button>
<div class="flex items-center space-x-2">
<%= if ssid_info.received_at do %>
<span class="text-xs text-slate-500 dark:text-slate-400">
{time_ago_in_words(ssid_info.received_at)}
</span>
<% end %>
<.link
navigate={~p"/info/#{ssid_info.callsign}"}
class="text-xs text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
title={gettext("Station info")}
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</.link>
</div>
</div>
<% end %>
</div>
</form>
</div>
<% end %>
<!-- Trail Duration -->
<div class="space-y-4">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 flex items-center space-x-2">
<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{gettext("Trail Duration")}</span>
</label>
<form phx-change="update_trail_duration" class="relative">
<select
name="trail_duration"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm bg-white dark:bg-slate-800 text-gray-900 dark:text-white appearance-none transition-all duration-200 hover:border-slate-400 dark:hover:border-slate-500"
>
<option value="1" selected={@trail_duration == "1"}>
{ngettext("1 Hour", "%{count} Hours", 1)}
</option>
<option value="6" selected={@trail_duration == "6"}>
{ngettext("1 Hour", "%{count} Hours", 6)}
</option>
<option value="12" selected={@trail_duration == "12"}>
{ngettext("1 Hour", "%{count} Hours", 12)}
</option>
<option value="24" selected={@trail_duration == "24"}>
{ngettext("1 Hour", "%{count} Hours", 24)}
</option>
<option value="48" selected={@trail_duration == "48"}>
{ngettext("1 Hour", "%{count} Hours", 48)}
</option>
<option value="168" selected={@trail_duration == "168"}>
{gettext("1 Week")}
</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</form>
</div>
<!-- Historical Data -->
<div class="space-y-4">
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 flex items-center space-x-2">
<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{gettext("Historical Data")}</span>
</label>
<form phx-change="update_historical_hours" class="relative">
<select
name="historical_hours"
class="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm bg-white dark:bg-slate-800 text-gray-900 dark:text-white appearance-none transition-all duration-200 hover:border-slate-400 dark:hover:border-slate-500"
>
<option value="1" selected={@historical_hours == "1"}>
{ngettext("1 Hour", "%{count} Hours", 1)}
</option>
<option value="3" selected={@historical_hours == "3"}>
{ngettext("1 Hour", "%{count} Hours", 3)}
</option>
<option value="6" selected={@historical_hours == "6"}>
{ngettext("1 Hour", "%{count} Hours", 6)}
</option>
<option value="12" selected={@historical_hours == "12"}>
{ngettext("1 Hour", "%{count} Hours", 12)}
</option>
<option value="24" selected={@historical_hours == "24"}>
{ngettext("1 Hour", "%{count} Hours", 24)}
</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</form>
</div>
<!-- Navigation -->
<div class="pt-4 border-t border-slate-200 dark:border-slate-700 space-y-3">
<div class="flex items-center space-x-2 text-sm text-slate-600 dark:text-slate-400 mb-3">
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"
/>
</svg>
<span class="font-medium">{gettext("Navigation")}</span>
</div>
<div class="text-sm">
<.navigation
variant={:vertical}
current_user={@current_user}
map_state={%{lat: @map_center.lat, lng: @map_center.lng, zoom: @map_zoom}}
tracked_callsign={@tracked_callsign}
/>
</div>
</div>
<!-- Last Update -->
<div class="pt-4 border-t border-slate-200 dark:border-slate-700 space-y-3">
<div class="flex items-center space-x-2 text-sm text-slate-600 dark:text-slate-400">
<svg class="w-4 h-4 text-slate-400" 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>
<span class="font-medium">{gettext("Last Update")}</span>
</div>
<div class="text-xs text-slate-500 dark:text-slate-400">
<%= if @last_update_at do %>
<div class="font-mono">{time_ago_in_words(@last_update_at)}</div>
<div class="font-mono text-slate-400 dark:text-slate-500">
{Calendar.strftime(@last_update_at, "%Y-%m-%d %H:%M UTC")}
<!-- Other SSIDs -->
<%= if @tracked_callsign != "" and @other_ssids != [] do %>
<div class="rounded-lg bg-gray-50 p-4 ring-1 ring-gray-950/5 dark:bg-gray-900/50 dark:ring-white/10">
<label class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
<svg class="h-3.5 w-3.5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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>
{gettext("Other SSIDs")}
</label>
<div class="space-y-1">
<%= for ssid_info <- @other_ssids do %>
<div class="group flex items-center justify-between rounded-md bg-white px-3 py-2 text-sm ring-1 ring-gray-950/5 hover:ring-indigo-300 dark:bg-gray-800 dark:ring-white/10 dark:hover:ring-indigo-500/50">
<button
phx-click="track_callsign"
phx-value-callsign={ssid_info.callsign}
class="font-semibold font-mono text-indigo-600 group-hover:text-indigo-500 cursor-pointer dark:text-indigo-400 dark:group-hover:text-indigo-300"
>
{ssid_info.callsign}
</button>
<div class="flex items-center gap-2">
<%= if ssid_info.received_at do %>
<span class="text-xs text-gray-500 dark:text-gray-400">
{time_ago_in_words(ssid_info.received_at)}
</span>
<% end %>
<.link
navigate={~p"/info/#{ssid_info.callsign}"}
class="text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400"
title={gettext("Station info")}
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</.link>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="font-mono text-slate-500 dark:text-slate-400">No updates yet</div>
<% end %>
</div>
<% end %>
<!-- Trail Duration -->
<div class="rounded-lg bg-gray-50 p-4 ring-1 ring-gray-950/5 dark:bg-gray-900/50 dark:ring-white/10">
<label class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
<svg class="h-3.5 w-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{gettext("Trail Duration")}
</label>
<form phx-change="update_trail_duration">
<select
name="trail_duration"
class="block w-full rounded-md bg-white px-3 py-2 text-sm text-gray-900 shadow-xs outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 dark:bg-gray-800 dark:text-white dark:outline-white/10 dark:focus:outline-indigo-500"
>
<option value="1" selected={@trail_duration == "1"}>
{ngettext("1 Hour", "%{count} Hours", 1)}
</option>
<option value="6" selected={@trail_duration == "6"}>
{ngettext("1 Hour", "%{count} Hours", 6)}
</option>
<option value="12" selected={@trail_duration == "12"}>
{ngettext("1 Hour", "%{count} Hours", 12)}
</option>
<option value="24" selected={@trail_duration == "24"}>
{ngettext("1 Hour", "%{count} Hours", 24)}
</option>
<option value="48" selected={@trail_duration == "48"}>
{ngettext("1 Hour", "%{count} Hours", 48)}
</option>
<option value="168" selected={@trail_duration == "168"}>
{gettext("1 Week")}
</option>
</select>
</form>
</div>
</div>
<!-- Deployment Information -->
<div class="pt-4 border-t border-slate-200 dark:border-slate-700 space-y-3">
<div class="flex items-center space-x-2 text-sm text-slate-600 dark:text-slate-400">
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
<!-- Historical Data -->
<div class="rounded-lg bg-gray-50 p-4 ring-1 ring-gray-950/5 dark:bg-gray-900/50 dark:ring-white/10">
<label class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
<svg class="h-3.5 w-3.5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{gettext("Historical Data")}
</label>
<form phx-change="update_historical_hours">
<select
name="historical_hours"
class="block w-full rounded-md bg-white px-3 py-2 text-sm text-gray-900 shadow-xs outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 dark:bg-gray-800 dark:text-white dark:outline-white/10 dark:focus:outline-indigo-500"
>
<option value="1" selected={@historical_hours == "1"}>
{ngettext("1 Hour", "%{count} Hours", 1)}
</option>
<option value="3" selected={@historical_hours == "3"}>
{ngettext("1 Hour", "%{count} Hours", 3)}
</option>
<option value="6" selected={@historical_hours == "6"}>
{ngettext("1 Hour", "%{count} Hours", 6)}
</option>
<option value="12" selected={@historical_hours == "12"}>
{ngettext("1 Hour", "%{count} Hours", 12)}
</option>
<option value="24" selected={@historical_hours == "24"}>
{ngettext("1 Hour", "%{count} Hours", 24)}
</option>
</select>
</form>
</div>
<!-- Navigation -->
<div class="border-t border-gray-200 pt-4 dark:border-white/10">
<label class="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
{gettext("Navigation")}
</label>
<nav class="space-y-1">
<.navigation
variant={:sidebar}
current_user={@current_user}
map_state={%{lat: @map_center.lat, lng: @map_center.lng, zoom: @map_zoom}}
tracked_callsign={@tracked_callsign}
/>
</svg>
<span class="font-medium">{gettext("Last Deploy")}</span>
</nav>
</div>
<div class="text-xs text-slate-500 dark:text-slate-400">
<div class="font-mono">{time_ago_in_words(@deployed_at)}</div>
<div class="font-mono text-slate-400">
{Calendar.strftime(@deployed_at, "%Y-%m-%d %H:%M UTC")}
<!-- Status Footer -->
<div class="border-t border-gray-200 pt-4 dark:border-white/10">
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
{gettext("Last Update")}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
<%= if @last_update_at do %>
<div class="font-mono font-medium">{time_ago_in_words(@last_update_at)}</div>
<div class="font-mono text-gray-400 dark:text-gray-500">
{Calendar.strftime(@last_update_at, "%H:%M UTC")}
</div>
<% else %>
<div class="font-mono text-gray-400">--</div>
<% end %>
</div>
</div>
<div>
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
{gettext("Last Deploy")}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
<div class="font-mono font-medium">{time_ago_in_words(@deployed_at)}</div>
<div class="font-mono text-gray-400 dark:text-gray-500">
{Calendar.strftime(@deployed_at, "%H:%M UTC")}
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -16,24 +16,36 @@
</div>
</div>
<% else %>
<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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10">
<div class="px-6 py-6">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr>
<th>Time</th>
<th>Sender</th>
<th>Data Type</th>
<th>Information</th>
<th>Path</th>
<th>Device</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
Time
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
Sender
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
Data Type
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
Information
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
Path
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
Device
</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-gray-100 dark:divide-white/5">
<%= for packet <- @all_packets do %>
<tr>
<td>
<tr class="even:bg-gray-50 dark:even:bg-white/[0.025]">
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-sm font-mono text-gray-500 dark:text-gray-400">
<%= case packet.received_at do %>
<% %DateTime{} = dt -> %>
@ -50,16 +62,16 @@
<% end %>
</span>
</td>
<td>
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-sm font-medium">{packet.sender}</span>
</td>
<td>
<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">
<td class="whitespace-nowrap py-3 pr-3">
<span class="inline-flex items-center rounded-md bg-indigo-50 px-2 py-1 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-700/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:ring-indigo-400/30">
{packet.data_type}
</span>
</td>
<td>
<span class="text-sm font-mono">
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-sm font-mono text-gray-700 dark:text-gray-300">
<% info_field = (Map.get(packet, :data) || %{})["information_field"] || "" %>
<%= if String.length(info_field) > 50 do %>
<span title={info_field}>
@ -70,12 +82,12 @@
<% end %>
</span>
</td>
<td>
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
{packet.path}
</span>
</td>
<td>
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
<%= if packet.device_model || packet.device_vendor do %>
{[packet.device_model, packet.device_vendor]
@ -117,7 +129,7 @@
</div>
<% end %>
<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-4">
<div class="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10 mt-4">
<div class="px-6 py-4">
<div class="flex items-center justify-between text-sm">
<div class="flex items-center space-x-4">

View file

@ -1,25 +1,43 @@
<div class="container mx-auto max-w-7xl py-8">
<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="bg-white shadow-xs sm:rounded-lg ring-1 ring-gray-950/5 dark:bg-gray-800/50 dark:shadow-none dark:ring-white/10">
<div class="px-6 py-6">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr>
<th>{gettext("Sender")}</th>
<th>{gettext("SSID")}</th>
<th>{gettext("Base Callsign")}</th>
<th>{gettext("Data Type")}</th>
<th>{gettext("Symbol")}</th>
<th>{gettext("Path")}</th>
<th>{gettext("Latitude")}</th>
<th>{gettext("Longitude")}</th>
<th>{gettext("Device")}</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Sender")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("SSID")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Base Callsign")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Data Type")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Symbol")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Path")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Latitude")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Longitude")}
</th>
<th class="sticky top-0 bg-white py-3 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
{gettext("Device")}
</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-gray-100 dark:divide-white/5">
<%= for packet <- @packets do %>
<tr>
<td>
<tr class="even:bg-gray-50 dark:even:bg-white/[0.025]">
<td class="whitespace-nowrap py-3 pr-3">
<.link
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
@ -27,12 +45,10 @@
{Map.get(packet, :sender, Map.get(packet, "sender", ""))}
</.link>
</td>
<td>
<span class="text-sm">
{Map.get(packet, :ssid, Map.get(packet, "ssid", ""))}
</span>
<td class="whitespace-nowrap py-3 pr-3 text-sm text-gray-500 dark:text-gray-400">
{Map.get(packet, :ssid, Map.get(packet, "ssid", ""))}
</td>
<td>
<td class="whitespace-nowrap py-3 pr-3">
<.link
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
@ -40,13 +56,13 @@
{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}
</.link>
</td>
<td>
<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">
<td class="whitespace-nowrap py-3 pr-3">
<span class="inline-flex items-center rounded-md bg-indigo-50 px-2 py-1 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-700/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:ring-indigo-400/30">
{Map.get(packet, :data_type, Map.get(packet, "data_type", ""))}
</span>
</td>
<td>
<span class="text-xs font-mono">
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-xs font-mono text-gray-700 dark:text-gray-300">
<% data = Map.get(packet, :data_extended) || %{}
raw_table =
@ -57,23 +73,23 @@
{table}{code}
</span>
</td>
<td>
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
{Map.get(packet, :path, Map.get(packet, "path", ""))}
</span>
</td>
<td>
<span class="text-xs font-mono">
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-xs font-mono text-gray-700 dark:text-gray-300">
{format_coordinate(extract_coordinate(packet, :lat))}
</span>
</td>
<td>
<span class="text-xs font-mono">
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-xs font-mono text-gray-700 dark:text-gray-300">
{format_coordinate(extract_coordinate(packet, :lon))}
</span>
</td>
<td>
<span class="text-xs font-mono">
<td class="whitespace-nowrap py-3 pr-3">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
{Map.get(packet, :device_identifier, Map.get(packet, "device_identifier", ""))}
</span>
</td>