Remove DaisyUI, migrate to pure Tailwind CSS with TailwindUI patterns

Replace all DaisyUI component classes with pure Tailwind CSS utility
classes following TailwindUI conventions. Use dark: variants for dark
mode instead of DaisyUI's data-theme semantic color system.

- Remove DaisyUI plugin and theme vendor files
- Configure Tailwind v4 custom dark variant for data-theme attribute
- Migrate all components: cards, tables, badges, buttons, forms,
  navigation, dropdowns, modals, alerts, star ratings, spinners
- Update all auth pages (login, register, settings, password reset,
  confirmation) to TailwindUI card layout
- Update content pages (about, API docs, status, packets, bad packets,
  info, weather) to TailwindUI patterns
- Replace opacity-based text styling with semantic gray colors
- Update test assertions to match new class names
This commit is contained in:
Graham McIntire 2026-02-19 10:07:41 -06:00
parent f9af61f29a
commit 4de48294c6
No known key found for this signature in database
26 changed files with 1640 additions and 2540 deletions

View file

@ -12,86 +12,8 @@
The heroicons installation itself is managed by your mix.exs */
@plugin "../vendor/heroicons";
/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
@plugin "../vendor/daisyui" {
themes: false;
}
/* daisyUI theme plugin. You can update this file by fetching the latest version with:
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
@plugin "../vendor/daisyui-theme" {
name: "dark";
default: false;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(30.33% 0.016 252.42);
--color-base-200: oklch(25.26% 0.014 253.1);
--color-base-300: oklch(20.15% 0.012 254.09);
--color-base-content: oklch(97.807% 0.029 256.847);
--color-primary: oklch(58% 0.233 277.117);
--color-primary-content: oklch(96% 0.018 272.314);
--color-secondary: oklch(58% 0.233 277.117);
--color-secondary-content: oklch(96% 0.018 272.314);
--color-accent: oklch(60% 0.25 292.717);
--color-accent-content: oklch(96% 0.016 293.756);
--color-neutral: oklch(37% 0.044 257.287);
--color-neutral-content: oklch(98% 0.003 247.858);
--color-info: oklch(58% 0.158 241.966);
--color-info-content: oklch(97% 0.013 236.62);
--color-success: oklch(60% 0.118 184.704);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
@plugin "../vendor/daisyui-theme" {
name: "light";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(98% 0 0);
--color-base-200: oklch(96% 0.001 286.375);
--color-base-300: oklch(92% 0.004 286.32);
--color-base-content: oklch(21% 0.006 285.885);
--color-primary: oklch(70% 0.213 47.604);
--color-primary-content: oklch(98% 0.016 73.684);
--color-secondary: oklch(55% 0.027 264.364);
--color-secondary-content: oklch(98% 0.002 247.839);
--color-accent: oklch(0% 0 0);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(44% 0.017 285.786);
--color-neutral-content: oklch(98% 0 0);
--color-info: oklch(62% 0.214 259.815);
--color-info-content: oklch(97% 0.014 254.604);
--color-success: oklch(70% 0.14 182.503);
--color-success-content: oklch(98% 0.014 180.72);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.21875rem;
--size-field: 0.21875rem;
--border: 1.5px;
--depth: 1;
--noise: 0;
}
/* Dark mode uses data-theme="dark" attribute on <html>, set via localStorage */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* Add variants based on LiveView classes */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@ -105,27 +27,6 @@
/* This file is for your main application CSS */
/* Navbar dark mode fixes */
[data-theme="dark"] .navbar,
[data-theme="dark"] .navbar a,
[data-theme="dark"] .navbar .menu,
[data-theme="dark"] .navbar .btn,
[data-theme="dark"] .navbar .btn-ghost {
color: #fff !important;
}
[data-theme="dark"] .navbar .btn-ghost:hover {
background-color: oklch(25.26% 0.014 253.1);
color: #fff !important;
}
[data-theme="dark"] .navbar .text-primary {
color: oklch(58% 0.233 277.117) !important;
}
/* Input field dark mode fixes */
/* Marker cluster styles */
.marker-cluster-small {
background-color: rgba(16, 185, 129, 0.8);
@ -333,59 +234,3 @@
.historical-dot-marker {
z-index: 15 !important;
}
/* Input field dark mode fixes - Override DaisyUI CSS variables for dark theme */
[data-theme="dark"] {
--color-base-100: #2d3748 !important;
--color-base-200: #1a202c !important;
--color-base-300: #171923 !important;
--color-base-content: #f7fafc !important;
--input-bg: #2d3748;
--input-color: #f7fafc;
--input-border: #4a5568;
}
/* Ultra-specific selector to override DaisyUI */
html[data-theme="dark"] .card .input,
html[data-theme="dark"] .card .input-bordered,
html[data-theme="dark"] .card input.input,
html[data-theme="dark"] .card input.input-bordered,
html[data-theme="dark"] .card input[type="email"],
html[data-theme="dark"] .card input[type="password"],
html[data-theme="dark"] .form-control input.input,
html[data-theme="dark"] .form-control input.input-bordered,
html[data-theme="dark"] .form-control input[type="email"],
html[data-theme="dark"] .form-control input[type="password"] {
background-color: #2d3748 !important;
color: #f7fafc !important;
border-color: #4a5568 !important;
}
html[data-theme="dark"] .card .input:focus,
html[data-theme="dark"] .card .input-bordered:focus,
html[data-theme="dark"] .card input.input:focus,
html[data-theme="dark"] .card input.input-bordered:focus,
html[data-theme="dark"] .card input[type="email"]:focus,
html[data-theme="dark"] .card input[type="password"]:focus,
html[data-theme="dark"] .form-control input.input:focus,
html[data-theme="dark"] .form-control input.input-bordered:focus,
html[data-theme="dark"] .form-control input[type="email"]:focus,
html[data-theme="dark"] .form-control input[type="password"]:focus {
background-color: #2d3748 !important;
color: #f7fafc !important;
border-color: #6366f1 !important;
}
html[data-theme="dark"] .input::placeholder,
html[data-theme="dark"] input.input::placeholder,
html[data-theme="dark"] input[type="email"]::placeholder,
html[data-theme="dark"] input[type="password"]::placeholder {
color: oklch(60% 0.029 256.847) !important;
}
html[data-theme="dark"] .checkbox,
html[data-theme="dark"] input.checkbox {
background-color: var(--input-bg) !important;
border-color: var(--input-border) !important;
}

View file

@ -15,7 +15,7 @@ html, body {
}
/* Optimize header for mobile */
.navbar {
.site-header {
padding: 0.5rem;
position: sticky;
top: 0;
@ -84,10 +84,8 @@ html, body {
}
/* Mobile-friendly buttons */
.btn, button {
button {
min-height: 44px;
padding: 12px 16px;
font-size: 16px;
}
/* Hide non-essential elements on small screens */
@ -152,7 +150,7 @@ html, body {
}
/* Safe area insets for devices with notches */
.navbar {
.site-header {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
@ -165,7 +163,7 @@ html, body {
/* Landscape mode optimizations */
@media (max-width: 768px) and (orientation: landscape) {
.navbar {
.site-header {
padding: 0.25rem 0.5rem;
}

File diff suppressed because one or more lines are too long

1031
assets/vendor/daisyui.js vendored

File diff suppressed because one or more lines are too long

View file

@ -224,7 +224,7 @@ defmodule AprsmeWeb.CoreComponents do
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="space-y-8 bg-base-100 mt-10">
<div class="space-y-8 mt-10">
{render_slot(@inner_block, f)}
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
{render_slot(action, f)}
@ -433,32 +433,47 @@ defmodule AprsmeWeb.CoreComponents do
def header(assigns) do
~H"""
<div class="navbar bg-base-200 shadow-lg">
<div class="navbar-start">
<.link navigate="/" class="btn btn-ghost text-xl font-bold text-primary">
aprs.me
<%= if @dev_mode do %>
<span class="badge badge-warning badge-sm ml-2">DEV</span>
<% end %>
</.link>
</div>
<div class="navbar-center hidden lg:flex">
<.navigation variant={:horizontal} current_user={@current_user} />
</div>
<div class="navbar-end">
<.theme_selector />
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<.navigation variant={:vertical} current_user={@current_user} />
</ul>
<header class="site-header bg-white shadow-sm dark:bg-gray-800/50 dark:shadow-none dark:border-b dark:border-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"
>
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">
DEV
</span>
<% end %>
</.link>
</div>
</div>
</div>
<div class="hidden lg:flex lg:items-center lg:gap-x-8">
<.navigation variant={:horizontal} current_user={@current_user} />
</div>
<div class="flex items-center gap-2">
<.theme_selector />
<div class="relative lg:hidden">
<button
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"
>
<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" />
</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"
>
<.navigation variant={:vertical} current_user={@current_user} />
</div>
</div>
</div>
</nav>
</header>
"""
end
@ -467,46 +482,58 @@ defmodule AprsmeWeb.CoreComponents do
"""
def theme_selector(assigns) do
~H"""
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg class="w-5 h-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<div class="relative">
<button
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"
>
<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" />
</svg>
</button>
<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"
>
<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"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clip-rule="evenodd"
/>
</svg>
{gettext("Light")}
</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"
>
<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" />
</svg>
{gettext("Dark")}
</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"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"
/>
</svg>
{gettext("Auto")}
</button>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "light"})} class="flex items-center gap-2">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clip-rule="evenodd"
/>
</svg>
{gettext("Light")}
</button>
</li>
<li>
<button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "dark"})} class="flex items-center gap-2">
<svg class="w-4 h-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" />
</svg>
{gettext("Dark")}
</button>
</li>
<li>
<button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "auto"})} class="flex items-center gap-2">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"
/>
</svg>
{gettext("Auto")}
</button>
</li>
</ul>
</div>
"""
end
@ -746,74 +773,98 @@ defmodule AprsmeWeb.CoreComponents do
~H"""
<%= if @variant == :horizontal do %>
<ul class="menu menu-horizontal px-1">
<li>
<.link navigate={@home_url} class="text-base-content hover:text-base-content/70">
{gettext("Home")}
</.link>
</li>
<li><.link navigate="/api" class="text-base-content hover:text-base-content/70">API</.link></li>
<li>
<.link navigate="/about" class="text-base-content hover:text-base-content/70">
{gettext("About")}
</.link>
</li>
<%= if @current_user do %>
<li>
<.link navigate="/users/settings" class="text-base-content hover:text-base-content/70">
{gettext("Settings")}
</.link>
</li>
<li>
<.link href="/users/log_out" method="delete" class="text-base-content hover:text-base-content/70">
{gettext("Log out")}
</.link>
</li>
<% else %>
<li>
<.link navigate="/users/register" class="text-base-content hover:text-base-content/70">
{gettext("Register")}
</.link>
</li>
<li>
<.link navigate="/users/log_in" class="text-base-content hover:text-base-content/70">
{gettext("Log in")}
</.link>
</li>
<% end %>
</ul>
<% else %>
<li>
<.link navigate={@home_url} class="text-base-content hover:text-base-content/70">
{gettext("Home")}
</.link>
</li>
<li><.link navigate="/api" class="text-base-content hover:text-base-content/70">API</.link></li>
<li>
<.link navigate="/about" class="text-base-content hover:text-base-content/70">{gettext("About")}</.link>
</li>
<.link
navigate={@home_url}
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark: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"
>
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"
>
{gettext("About")}
</.link>
<%= if @current_user do %>
<li>
<.link navigate="/users/settings" class="text-base-content hover:text-base-content/70">
{gettext("Settings")}
</.link>
</li>
<li>
<.link href="/users/log_out" method="delete" class="text-base-content hover:text-base-content/70">
{gettext("Log out")}
</.link>
</li>
<.link
navigate="/users/settings"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark: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"
>
{gettext("Log out")}
</.link>
<% else %>
<li>
<.link navigate="/users/register" class="text-base-content hover:text-base-content/70">
{gettext("Register")}
</.link>
</li>
<li>
<.link navigate="/users/log_in" class="text-base-content hover:text-base-content/70">
{gettext("Log in")}
</.link>
</li>
<.link
navigate="/users/register"
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark: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"
>
{gettext("Log in")}
</.link>
<% end %>
<% else %>
<.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"
>
{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"
>
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"
>
{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"
>
{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"
>
{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"
>
{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"
>
{gettext("Log in")}
</.link>
<% end %>
<% end %>
"""

View file

@ -28,8 +28,20 @@ defmodule AprsmeWeb.Components.InfoMapComponent do
data-callsign={@callsign}
data-symbol-html={@symbol_html}
>
<div id={"#{@id}-loading"} class="h-full w-full bg-base-200 flex items-center justify-center">
<span class="loading loading-spinner loading-lg"></span>
<div id={"#{@id}-loading"} class="h-full w-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
<svg
class="animate-spin h-8 w-8 text-gray-400 dark:text-gray-500"
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" />
<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"
/>
</svg>
</div>
</div>
"""

View file

@ -5,6 +5,6 @@ defmodule AprsmeWeb.Layouts do
embed_templates "layouts/*"
def body_class(_assigns) do
["bg-base-100 antialiased"]
["bg-white dark:bg-gray-900 text-gray-900 dark:text-white antialiased"]
end
end

View file

@ -29,8 +29,8 @@
height: 100vh !important;
}
/* Map page specific styles - hide navbar header */
body:has(#main-content[data-map-page="true"]) .navbar {
/* Map page specific styles - hide site header */
body:has(#main-content[data-map-page="true"]) .site-header {
display: none;
}
@ -94,7 +94,7 @@
right: 0;
height: 100vh;
width: 352px;
background: var(--color-base-100);
background: white;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
z-index: 1000;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@ -153,7 +153,7 @@
<.header dev_mode={Application.get_env(:aprsme, :dev_routes, false)} current_user={@current_user} />
<main
id="main-content"
class={if assigns[:map_page], do: "min-h-screen bg-base-100", else: "bg-base-100"}
class={if assigns[:map_page], do: "min-h-screen", else: ""}
data-map-page={to_string(assigns[:map_page] || false)}
>
<div>

View file

@ -1,30 +1,45 @@
<div class="container mx-auto max-w-2xl py-12">
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body prose max-w-none">
<h2 class="card-title text-3xl">{gettext("Under heavy construction")}</h2>
<p>
<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>
<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-base-200 p-6 rounded-lg mb-8">
<h4 class="text-lg font-semibold mb-3">{gettext("Built with Modern Technologies")}</h4>
<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">
{gettext("Built with Modern Technologies")}
</h4>
<div class="flex flex-wrap gap-2">
<span class="badge badge-primary">Elixir</span>
<span class="badge badge-success">Phoenix</span>
<span class="badge badge-secondary">LiveView</span>
<span class="badge badge-warning">PostgreSQL</span>
<span class="badge badge-error">Tailwind CSS</span>
<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">
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">
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">
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">
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">
Tailwind CSS
</span>
</div>
</div>
<h3 class="text-2xl font-semibold mb-4">{gettext("Join Our Community")}</h3>
<p>
<h3 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">{gettext("Join Our Community")}</h3>
<p class="text-gray-600 dark:text-gray-300">
Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.
</p>
<div class="text-center mt-12">
<a href="/" class="btn btn-primary btn-lg">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<a
href="/"
class="inline-flex items-center rounded-md bg-indigo-600 px-4 py-2.5 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 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -37,7 +52,7 @@
</div>
</div>
</div>
<div class="text-center text-base-content/60">
<div class="text-center text-gray-500 dark:text-gray-400">
<p class="text-sm">
&copy; 2025 aprs.me. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>

View file

@ -217,49 +217,57 @@ defmodule AprsmeWeb.ApiDocsLive do
<div class="container mx-auto max-w-6xl py-8">
<div class="mb-8">
<h1 class="text-4xl font-bold mb-4">APRS.me API Documentation</h1>
<p class="text-lg opacity-70">
<p class="text-lg text-gray-500 dark:text-gray-400">
RESTful JSON API for accessing APRS packet data and station information.
</p>
</div>
<!-- API Overview -->
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body">
<h2 class="card-title text-2xl">Overview</h2>
<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">
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white">Overview</h2>
<p class="mb-4">
The APRS.me API provides programmatic access to Amateur Radio APRS (Automatic Packet Reporting System)
data. All API endpoints return JSON data and follow RESTful conventions.
</p>
<div class="grid md:grid-cols-2 gap-6 mt-6">
<div class="bg-primary/10 p-4 rounded-lg">
<div class="bg-indigo-50 dark:bg-indigo-500/10 p-4 rounded-lg">
<h3 class="font-semibold mb-2">Base URL</h3>
<code class="text-sm bg-base-200 px-2 py-1 rounded">
<code class="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
https://aprs.me/api/v1
</code>
</div>
<div class="bg-success/10 p-4 rounded-lg">
<div class="bg-green-50 dark:bg-green-500/10 p-4 rounded-lg">
<h3 class="font-semibold mb-2">Content Type</h3>
<code class="text-sm bg-base-200 px-2 py-1 rounded">
<code class="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
application/json
</code>
</div>
</div>
<div class="alert alert-warning mt-6">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<div>
<h3 class="font-bold">Rate Limiting</h3>
<div class="text-xs">
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests. Rate limiting may be implemented in the future.
<div class="rounded-md bg-yellow-50 p-4 dark:bg-yellow-500/10 mt-6">
<div class="flex">
<svg
xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-6 w-6 text-yellow-700 dark:text-yellow-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<div class="ml-3">
<h3 class="text-sm font-bold text-yellow-700 dark:text-yellow-400">Rate Limiting</h3>
<div class="text-sm text-yellow-700 dark:text-yellow-400">
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests. Rate limiting may be implemented in the future.
</div>
</div>
</div>
</div>
@ -269,16 +277,18 @@ defmodule AprsmeWeb.ApiDocsLive do
<!-- API Endpoints -->
<div class="space-y-8">
<!-- Callsign Endpoint -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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-8">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title text-xl">Get Latest Packet by Callsign</h2>
<span class="badge badge-success">GET</span>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Get Latest Packet by Callsign</h2>
<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">
GET
</span>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium mb-2">Endpoint</h3>
<div class="bg-base-200 p-3 rounded-lg">
<div class="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg">
<code class="text-sm">GET /api/v1/callsign/{"{callsign}"}</code>
</div>
</div>
@ -294,21 +304,25 @@ defmodule AprsmeWeb.ApiDocsLive do
<div class="mb-4">
<h3 class="text-lg font-medium mb-2">Parameters</h3>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead class="text-left text-sm font-semibold text-gray-900 dark:text-white">
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
<th class="px-3 py-3.5">Parameter</th>
<th class="px-3 py-3.5">Type</th>
<th class="px-3 py-3.5">Required</th>
<th class="px-3 py-3.5">Description</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td class="font-mono">callsign</td>
<td>string</td>
<td>Yes</td>
<td>Amateur radio callsign with optional SSID (e.g., N0CALL or N0CALL-9)</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
callsign
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Yes</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Amateur radio callsign with optional SSID (e.g., N0CALL or N0CALL-9)
</td>
</tr>
</tbody>
</table>
@ -317,7 +331,7 @@ defmodule AprsmeWeb.ApiDocsLive do
<div class="mb-4">
<h3 class="text-lg font-medium mb-2">Example Request</h3>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto">
<div class="bg-gray-900 text-gray-100 dark:bg-gray-950 p-4 rounded-lg overflow-x-auto">
<pre><code>curl -X GET "https://aprs.me/api/v1/callsign/N0CALL-9" \
-H "Accept: application/json"</code></pre>
</div>
@ -327,7 +341,7 @@ defmodule AprsmeWeb.ApiDocsLive do
<h3 class="text-lg font-medium mb-2">Response Format</h3>
<h4 class="text-md font-medium mb-2">Success Response (200 OK)</h4>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto mb-4">
<div class="bg-gray-900 text-gray-100 dark:bg-gray-950 p-4 rounded-lg overflow-x-auto mb-4">
<pre><code><%= raw ~s|{
"data": {
"id": 12345,
@ -370,7 +384,7 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<h4 class="text-md font-medium mb-2">Not Found Response (404)</h4>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto mb-4">
<div class="bg-gray-900 text-gray-100 dark:bg-gray-950 p-4 rounded-lg overflow-x-auto mb-4">
<pre><code><%= raw ~s|{
"data": null,
"message": "No packets found for callsign N0CALL-9"
@ -378,7 +392,7 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<h4 class="text-md font-medium mb-2">Error Response (400)</h4>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto">
<div class="bg-gray-900 text-gray-100 dark:bg-gray-950 p-4 rounded-lg overflow-x-auto">
<pre><code><%= raw ~s|{
"error": {
"message": "Invalid callsign format",
@ -391,73 +405,97 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- Response Fields Documentation -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Response Fields</h2>
<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-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Response Fields</h2>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead class="text-left text-sm font-semibold text-gray-900 dark:text-white">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
<th class="px-3 py-3.5">Field</th>
<th class="px-3 py-3.5">Type</th>
<th class="px-3 py-3.5">Description</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td class="font-mono">id</td>
<td>integer</td>
<td>Unique packet identifier</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">id</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">integer</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Unique packet identifier</td>
</tr>
<tr>
<td class="font-mono">callsign</td>
<td>string</td>
<td>Full callsign with SSID (e.g., "N0CALL-9")</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">callsign</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Full callsign with SSID (e.g., "N0CALL-9")
</td>
</tr>
<tr>
<td class="font-mono">base_callsign</td>
<td>string</td>
<td>Base callsign without SSID</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
base_callsign
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Base callsign without SSID</td>
</tr>
<tr>
<td class="font-mono">ssid</td>
<td>string</td>
<td>SSID (Secondary Station Identifier), null if not present</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">ssid</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
SSID (Secondary Station Identifier), null if not present
</td>
</tr>
<tr>
<td class="font-mono">received_at</td>
<td>datetime</td>
<td>When the packet was received (ISO 8601 format)</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
received_at
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">datetime</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
When the packet was received (ISO 8601 format)
</td>
</tr>
<tr>
<td class="font-mono">position</td>
<td>object</td>
<td>Position data (latitude, longitude, course, speed, altitude)</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">position</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Position data (latitude, longitude, course, speed, altitude)
</td>
</tr>
<tr>
<td class="font-mono">symbol</td>
<td>object</td>
<td>APRS symbol information (code and table_id)</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">symbol</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
APRS symbol information (code and table_id)
</td>
</tr>
<tr>
<td class="font-mono">weather</td>
<td>object</td>
<td>Weather data if present (temperature, humidity, wind, etc.)</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">weather</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Weather data if present (temperature, humidity, wind, etc.)
</td>
</tr>
<tr>
<td class="font-mono">equipment</td>
<td>object</td>
<td>Equipment information (manufacturer, type)</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
equipment
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Equipment information (manufacturer, type)
</td>
</tr>
<tr>
<td class="font-mono">message</td>
<td>object</td>
<td>Message data if the packet is a message</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">message</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Message data if the packet is a message
</td>
</tr>
<tr>
<td class="font-mono">raw_packet</td>
<td>string</td>
<td>Original raw APRS packet as received</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
raw_packet
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Original raw APRS packet as received</td>
</tr>
</tbody>
</table>
@ -466,37 +504,53 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- HTTP Status Codes -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">HTTP Status Codes</h2>
<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-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">HTTP Status Codes</h2>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead class="text-left text-sm font-semibold text-gray-900 dark:text-white">
<tr>
<th>Status Code</th>
<th>Description</th>
<th class="px-3 py-3.5">Status Code</th>
<th class="px-3 py-3.5">Description</th>
</tr>
</thead>
<tbody>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td class="font-mono text-success">200 OK</td>
<td>Request successful, packet data returned</td>
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-green-600 dark:text-green-400">200 OK</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Request successful, packet data returned
</td>
</tr>
<tr>
<td class="font-mono text-warning">400 Bad Request</td>
<td>Invalid callsign format or malformed request</td>
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-yellow-600 dark:text-yellow-400">
400 Bad Request
</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Invalid callsign format or malformed request
</td>
</tr>
<tr>
<td class="font-mono text-error">404 Not Found</td>
<td>No packets found for the specified callsign</td>
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-red-600 dark:text-red-400">
404 Not Found
</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
No packets found for the specified callsign
</td>
</tr>
<tr>
<td class="font-mono text-error">408 Request Timeout</td>
<td>Request took too long to process</td>
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-red-600 dark:text-red-400">
408 Request Timeout
</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Request took too long to process</td>
</tr>
<tr>
<td class="font-mono text-error">500 Internal Server Error</td>
<td>Server error occurred while processing the request</td>
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-red-600 dark:text-red-400">
500 Internal Server Error
</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Server error occurred while processing the request
</td>
</tr>
</tbody>
</table>
@ -505,53 +559,61 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- Future Endpoints -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Planned Endpoints</h2>
<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-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Planned Endpoints</h2>
<p class="mb-4">
The following endpoints are planned for future releases:
</p>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div class="flex items-center justify-between p-3 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/callsign/{"{callsign}"}/history</code>
<p class="text-sm opacity-70 mt-1">Get historical packets for a callsign</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Get historical packets for a callsign</p>
</div>
<span class="badge badge-primary">Planned</span>
<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">
Planned
</span>
</div>
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div class="flex items-center justify-between p-3 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/packets/recent</code>
<p class="text-sm opacity-70 mt-1">Get recent packets with filtering options</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Get recent packets with filtering options</p>
</div>
<span class="badge badge-primary">Planned</span>
<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">
Planned
</span>
</div>
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div class="flex items-center justify-between p-3 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/packets/area</code>
<p class="text-sm opacity-70 mt-1">Get packets within a geographic area</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Get packets within a geographic area</p>
</div>
<span class="badge badge-primary">Planned</span>
<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">
Planned
</span>
</div>
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div class="flex items-center justify-between p-3 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/weather/{"{callsign}"}</code>
<p class="text-sm opacity-70 mt-1">Get weather data from weather stations</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Get weather data from weather stations</p>
</div>
<span class="badge badge-primary">Planned</span>
<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">
Planned
</span>
</div>
</div>
</div>
</div>
<!-- Interactive API Testing -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Test the API</h2>
<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-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Test the API</h2>
<p class="mb-4">
Try the API directly from this page. Enter a callsign to see the most recent packet data.
</p>
@ -559,10 +621,10 @@ defmodule AprsmeWeb.ApiDocsLive do
<div class="max-w-md">
<form phx-submit="test_api" class="space-y-4">
<div>
<label for="test_callsign" class="label">
<span class="label-text">Callsign (e.g., N0CALL or N0CALL-9)</span>
<label for="test_callsign" class="block text-sm/6 font-medium text-gray-900 dark:text-white">
Callsign (e.g., N0CALL or N0CALL-9)
</label>
<div class="flex space-x-2">
<div class="flex space-x-2 mt-2">
<input
type="text"
id="test_callsign"
@ -570,12 +632,29 @@ defmodule AprsmeWeb.ApiDocsLive do
value={@test_callsign}
phx-change="update_callsign"
placeholder="Enter callsign..."
class="input input-bordered flex-1"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500 flex-1"
disabled={@loading}
/>
<button type="submit" disabled={@loading or String.trim(@test_callsign) == ""} class="btn btn-primary">
<button
type="submit"
disabled={@loading or String.trim(@test_callsign) == ""}
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 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
<%= if @loading do %>
<span class="loading loading-spinner loading-sm"></span> Testing...
<span class="inline-flex items-center gap-1.5">
<svg
class="animate-spin h-4 w-4"
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 12h4z">
</path>
</svg>
Testing...
</span>
<% else %>
Test API
<% end %>
@ -586,21 +665,24 @@ defmodule AprsmeWeb.ApiDocsLive do
<!-- Error Display -->
<%= if @error do %>
<div class="alert alert-error mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{@error}</span>
<div class="rounded-md bg-red-50 p-4 dark:bg-red-500/10 mt-4">
<div class="flex">
<svg
xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-6 w-6 text-red-700 dark:text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span class="ml-3 text-sm text-red-700 dark:text-red-400">{@error}</span>
</div>
</div>
<% end %>
@ -609,9 +691,9 @@ defmodule AprsmeWeb.ApiDocsLive do
<div class="mt-4">
<h4 class="text-lg font-medium mb-2">API Response</h4>
<div class="bg-base-200 border rounded-lg p-4">
<div class="bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div class="flex items-center mb-3">
<svg class="h-5 w-5 text-primary mr-2" viewBox="0 0 20 20" fill="currentColor">
<svg class="h-5 w-5 text-indigo-600 dark:text-indigo-400 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
@ -621,7 +703,7 @@ defmodule AprsmeWeb.ApiDocsLive do
<span class="font-medium">JSON Response</span>
</div>
<div class="bg-neutral text-neutral-content p-4 rounded text-sm overflow-x-auto">
<div class="bg-gray-900 text-gray-100 dark:bg-gray-950 p-4 rounded text-sm overflow-x-auto">
<pre><%= @api_result %></pre>
</div>
</div>
@ -632,15 +714,15 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- Contact and Support -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Support</h2>
<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-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Support</h2>
<p class="mb-4">
This API is provided free of charge for amateur radio and educational purposes.
If you encounter issues or have suggestions for improvements, please reach out.
</p>
<div class="bg-base-200 p-4 rounded-lg">
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
<h3 class="font-semibold mb-2">Guidelines</h3>
<ul class="space-y-1">
<li> Use reasonable request rates to avoid overwhelming the service</li>

View file

@ -1,20 +1,32 @@
<div class="container mx-auto px-4 py-8">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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 justify-between mb-4">
<span class="text-sm text-base-content/70">
<span class="text-sm text-gray-500 dark:text-gray-400">
{gettext("Auto-refreshes every 5 seconds")}
</span>
<%= if @loading do %>
<span class="flex items-center gap-2 text-sm text-primary">
<span class="loading loading-spinner loading-xs"></span>
<span class="flex items-center gap-2 text-sm text-indigo-600 dark:text-indigo-400">
<svg
class="animate-spin h-4 w-4"
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" />
<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"
/>
</svg>
{gettext("Updating...")}
</span>
<% end %>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead>
<tr>
<th>{gettext("Attempted At")}</th>
@ -35,18 +47,18 @@
</span>
</td>
<td>
<div class="badge badge-error badge-sm">
<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">
{bad_packet.error_type || gettext("Unknown")}
</div>
</span>
</td>
<td>
<span class="text-sm text-error font-mono">
<span class="text-sm text-red-600 dark:text-red-400 font-mono">
{bad_packet.error_message || gettext("No error message")}
</span>
</td>
<td>
<div class="max-w-md">
<div class="text-xs font-mono text-base-content/80 break-all">
<div class="text-xs font-mono text-gray-600 dark:text-gray-300 break-all">
<%= if is_binary(bad_packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(bad_packet.raw_packet)}
<% else %>
@ -64,11 +76,16 @@
</div>
<%= if length(@bad_packets) == 0 do %>
<div class="card bg-base-100 shadow-xl mt-8">
<div class="card-body text-center">
<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-8">
<div class="px-6 py-8 text-center">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 bg-success/20 rounded-full flex items-center justify-center">
<svg class="w-8 h-8 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="w-16 h-16 bg-green-100 dark:bg-green-500/20 rounded-full flex items-center justify-center">
<svg
class="w-8 h-8 text-green-600 dark:text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -79,7 +96,7 @@
</div>
</div>
<h3 class="text-lg font-semibold">{gettext("No bad packets")}</h3>
<p class="text-base-content/70">{gettext("All packets are parsing successfully!")}</p>
<p class="text-gray-500 dark:text-gray-400">{gettext("All packets are parsing successfully!")}</p>
</div>
</div>
<% end %>

View file

@ -1,8 +1,8 @@
<!-- Leaflet is already loaded via vendor bundles in the layout -->
<div class="min-h-screen bg-base-200">
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- Page header -->
<div class="bg-base-100 shadow-sm">
<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">
@ -11,7 +11,7 @@
{gettext("APRS Station")}
</h1>
<div class="ml-3 flex items-center space-x-3">
<span class="badge badge-primary">
<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>
<%= if @packet do %>
@ -30,11 +30,17 @@
</div>
<div class="mt-3 flex md:ml-4 md:mt-0">
<div class="flex space-x-2">
<.link navigate={~p"/packets/#{@callsign}"} class="btn btn-outline btn-sm">
<.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="btn btn-primary btn-sm">
<.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 %>
@ -49,11 +55,17 @@
<!-- Station details -->
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mb-6 lg:grid-rows-1">
<!-- Position information -->
<div class="card bg-base-100 shadow-xl h-full">
<div class="card-body">
<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 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<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"
@ -68,14 +80,14 @@
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Location")}</dt>
<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 opacity-70">{gettext("Grid Square")}</dt>
<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} -> %>
@ -87,7 +99,7 @@
</div>
<% end %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Last Activity")}</dt>
<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
@ -98,35 +110,35 @@
>
{AprsmeWeb.TimeHelpers.time_ago_in_words(@packet.received_at)}
</div>
<div class="text-xs opacity-70 font-mono">
<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="opacity-70">{gettext("Unknown")}</span>
<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 opacity-70">{gettext("Altitude")}</dt>
<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 opacity-70">{gettext("Course")}</dt>
<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 opacity-70">{gettext("Speed")}</dt>
<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 opacity-70">{gettext("Comment")}</dt>
<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)}
@ -138,31 +150,31 @@
<% end %>
<%= if @packet.phg_power || @packet.phg_height || @packet.phg_gain do %>
<div class="col-span-2">
<dt class="text-xs font-medium opacity-70 mb-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="opacity-70">{gettext("Power:")}</span>
<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="opacity-70">{gettext("HAAT:")}</span>
<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="opacity-70">{gettext("Gain:")}</span>
<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="opacity-70">{gettext("Dir:")}</span>
<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")}
@ -181,7 +193,7 @@
<!-- Map showing station location -->
<%= if @packet && @packet.lat && @packet.lon do %>
<div class="card bg-base-100 shadow-xl h-full">
<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
@ -199,12 +211,12 @@
<!-- 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="card bg-base-100 shadow-xl">
<div class="card-body">
<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 opacity-70"
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
@ -223,7 +235,7 @@
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Device")}</dt>
<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 %>
{[
@ -237,41 +249,44 @@
({@packet.device_class})
<% end %>
<% else %>
<span class="opacity-70 font-mono">
<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 opacity-70">{gettext("Path")}</dt>
<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="link link-primary">
<.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="opacity-50">,</span>
<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 opacity-70">{gettext("Decoded Path")}</dt>
<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 opacity-70">{gettext("Raw Packet")}</dt>
<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)}
@ -286,12 +301,12 @@
<!-- Other SSIDs -->
<%= if length(@other_ssids) > 0 do %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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 opacity-70"
class="h-4 w-4 text-gray-400 dark:text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
@ -309,7 +324,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-sm table-zebra">
<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">
@ -328,7 +343,10 @@
<tr>
<td class="font-medium">
<div class="flex items-center space-x-2">
<.link navigate={~p"/info/#{ssid_info.callsign}"} class="link link-primary">
<.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>
<%= if ssid_info.packet do %>
@ -345,7 +363,7 @@
<% end %>
</div>
</td>
<td class="opacity-70">
<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(
@ -367,7 +385,7 @@
-
<% end %>
</td>
<td class="opacity-70">
<td class="text-gray-500 dark:text-gray-400">
<%= if ssid_info.received_at do %>
<span
class="text-sm"
@ -378,7 +396,7 @@
{AprsmeWeb.TimeHelpers.time_ago_in_words(ssid_info.received_at)}
</span>
<% else %>
<span class="text-sm opacity-70">{gettext("Unknown")}</span>
<span class="text-sm text-gray-500 dark:text-gray-400">{gettext("Unknown")}</span>
<% end %>
</td>
</tr>
@ -396,11 +414,17 @@
</div>
<!-- Neighboring stations -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<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"
@ -413,7 +437,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<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">
@ -432,7 +456,10 @@
<tr>
<td class="font-medium">
<div class="flex items-center space-x-2">
<.link navigate={~p"/info/#{neighbor.callsign}"} class="link link-primary">
<.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>
<%= if neighbor.packet do %>
@ -449,14 +476,14 @@
<% end %>
</div>
</td>
<td class="opacity-70">
<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="opacity-70">
<td class="text-gray-500 dark:text-gray-400">
<%= if neighbor.last_heard do %>
<div
class="font-semibold"
@ -466,11 +493,11 @@
>
{neighbor.last_heard.time_ago}
</div>
<div class="text-xs opacity-70 font-mono">
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{neighbor.last_heard.formatted}
</div>
<% else %>
<span class="opacity-70">{gettext("Unknown")}</span>
<span class="text-gray-500 dark:text-gray-400">{gettext("Unknown")}</span>
<% end %>
</td>
</tr>
@ -483,11 +510,17 @@
<!-- Stations that heard this callsign on RF -->
<%= if length(@heard_by_stations) > 0 do %>
<div class="card bg-base-100 shadow-xl mt-6">
<div class="card-body">
<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 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<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"
@ -504,7 +537,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<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">
@ -528,11 +561,14 @@
<%= for station <- @heard_by_stations do %>
<tr>
<td class="font-medium">
<.link navigate={~p"/info/#{station.digipeater}"} class="link link-primary">
<.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="opacity-70">
<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")}
@ -541,12 +577,12 @@
-
<% end %>
</td>
<td class="opacity-70">
<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 opacity-70 font-mono">
<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 %>
@ -556,11 +592,11 @@
<td>
{station.packet_count}
</td>
<td class="opacity-70">
<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 opacity-70 font-mono">
<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 %>
@ -579,11 +615,17 @@
<!-- Stations heard directly by this callsign -->
<%= if length(@stations_heard_by) > 0 do %>
<div class="card bg-base-100 shadow-xl mt-6">
<div class="card-body">
<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 opacity-70" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<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"
@ -600,7 +642,7 @@
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<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">
@ -624,11 +666,14 @@
<%= for station <- @stations_heard_by do %>
<tr>
<td class="font-medium">
<.link navigate={~p"/info/#{station.station}"} class="link link-primary">
<.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="opacity-70">
<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")}
@ -637,12 +682,12 @@
-
<% end %>
</td>
<td class="opacity-70">
<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 opacity-70 font-mono">
<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 %>
@ -652,11 +697,11 @@
<td>
{station.packet_count}
</td>
<td class="opacity-70">
<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 opacity-70 font-mono">
<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 %>
@ -675,7 +720,7 @@
<% else %>
<div class="text-center py-8">
<svg
class="mx-auto h-8 w-8 opacity-70"
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"
@ -689,7 +734,7 @@
/>
</svg>
<h3 class="mt-2 text-sm font-semibold">{gettext("No data available")}</h3>
<p class="mt-1 text-sm opacity-70">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{gettext("No recent packet data available for this callsign.")}
</p>
</div>

View file

@ -1682,14 +1682,14 @@ defmodule AprsmeWeb.MapLive.Index do
</svg>
<span class="font-medium">{gettext("Navigation")}</span>
</div>
<ul class="menu menu-sm text-sm">
<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}
/>
</ul>
</div>
</div>
<!-- Last Update -->

View file

@ -1,6 +1,6 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
<%= if @error do %>
<div class="alert alert-error">
<div class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
@ -16,10 +16,10 @@
</div>
</div>
<% else %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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="overflow-x-auto">
<table class="table table-zebra">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead>
<tr>
<th>Time</th>
@ -34,7 +34,7 @@
<%= for packet <- @all_packets do %>
<tr>
<td>
<span class="text-sm font-mono opacity-70">
<span class="text-sm font-mono text-gray-500 dark:text-gray-400">
<%= case packet.received_at do %>
<% %DateTime{} = dt -> %>
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S")}
@ -54,7 +54,7 @@
<span class="text-sm font-medium">{packet.sender}</span>
</td>
<td>
<span class="badge badge-primary">
<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">
{packet.data_type}
</span>
</td>
@ -70,12 +70,12 @@
</span>
</td>
<td>
<span class="text-xs font-mono opacity-70">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
{packet.path}
</span>
</td>
<td>
<span class="text-xs font-mono opacity-70">
<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]
|> Enum.reject(&is_nil/1)
@ -100,7 +100,7 @@
<%= if length(@all_packets) == 0 do %>
<div class="text-center py-12">
<div class="flex justify-center mb-4">
<svg class="w-16 h-16 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-16 h-16 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -110,14 +110,14 @@
</svg>
</div>
<h3 class="text-lg font-medium mb-2">No packets found for {@callsign}</h3>
<p class="opacity-70">
<p class="text-gray-500 dark:text-gray-400">
Packets will appear here as they are received live or if there are stored packets for this callsign.
</p>
</div>
<% end %>
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<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="px-6 py-4">
<div class="flex items-center justify-between text-sm">
<div class="flex items-center space-x-4">
<span>

View file

@ -1,8 +1,8 @@
<div class="container mx-auto max-w-7xl py-8">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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="overflow-x-auto">
<table class="table table-zebra">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead>
<tr>
<th>{gettext("Sender")}</th>
@ -22,7 +22,7 @@
<td>
<.link
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
class="link link-primary font-medium"
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{Map.get(packet, :sender, Map.get(packet, "sender", ""))}
</.link>
@ -35,13 +35,13 @@
<td>
<.link
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
class="link link-primary font-medium"
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}
</.link>
</td>
<td>
<span class="badge badge-primary">
<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">
{Map.get(packet, :data_type, Map.get(packet, "data_type", ""))}
</span>
</td>
@ -58,7 +58,7 @@
</span>
</td>
<td>
<span class="text-xs font-mono opacity-70">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
{Map.get(packet, :path, Map.get(packet, "path", ""))}
</span>
</td>
@ -148,7 +148,7 @@
<%= if length(@packets) == 0 do %>
<div class="text-center py-12">
<div class="flex justify-center mb-4">
<svg class="w-16 h-16 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-16 h-16 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -158,7 +158,7 @@
</svg>
</div>
<h3 class="text-lg font-medium mb-2">{gettext("No packets")}</h3>
<p class="opacity-70">{gettext("Packets will appear here as they are received.")}</p>
<p class="text-gray-500 dark:text-gray-400">{gettext("Packets will appear here as they are received.")}</p>
</div>
<% end %>
</div>

View file

@ -8,104 +8,108 @@ defmodule AprsmeWeb.StatusLive.ConnectionCardComponent do
~H"""
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">APRS-IS Connection</h2>
<div class="card bg-base-200">
<div class="card-body">
<div class="rounded-lg bg-gray-50 p-6 dark:bg-gray-700/50">
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Status:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Status:</span>
<%= if @aprs_status.connected do %>
<div class="badge badge-success gap-1">
<div class="w-2 h-2 bg-current rounded-full"></div>
<span class="inline-flex items-center gap-x-1.5 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">
<svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
<circle cx="3" cy="3" r="3" />
</svg>
Connected
</div>
</span>
<% else %>
<div class="badge badge-error gap-1">
<div class="w-2 h-2 bg-current rounded-full"></div>
<span class="inline-flex items-center gap-x-1.5 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">
<svg class="h-1.5 w-1.5 fill-red-500" viewBox="0 0 6 6" aria-hidden="true">
<circle cx="3" cy="3" r="3" />
</svg>
Disconnected
</div>
</span>
<% end %>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Server:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Server:</span>
<span class="text-sm font-mono">
{@aprs_status.server}:{@aprs_status.port}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Login ID:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Login ID:</span>
<span class="text-sm font-mono">{@aprs_status.login_id}</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Connected Since:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Connected Since:</span>
<span class="text-sm">
<%= if @aprs_status.connected_at do %>
{Calendar.strftime(@aprs_status.connected_at, "%Y-%m-%d %H:%M:%S UTC")}
<% else %>
<span class="opacity-50">Not connected</span>
<span class="text-gray-400 dark:text-gray-500">Not connected</span>
<% end %>
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Uptime:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Uptime:</span>
<span class={[
"text-sm font-medium",
if(@aprs_status.connected, do: "text-success", else: "text-error")
if(@aprs_status.connected, do: "text-green-600 dark:text-green-400", else: "text-red-600 dark:text-red-400")
]}>
{AprsmeWeb.StatusLive.Index.format_uptime(@aprs_status.uptime_seconds)}
</span>
</div>
<div class="flex items-center col-span-2">
<span class="text-sm font-medium opacity-70 mr-2">Filter:</span>
<div class="badge badge-outline font-mono">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Filter:</span>
<span class="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-600 font-mono dark:bg-gray-400/10 dark:text-gray-400">
{@aprs_status.filter}
</div>
</span>
</div>
</div>
<!-- Packet Statistics -->
<div class="divider"></div>
<div class="border-t border-gray-200 dark:border-white/10 my-4"></div>
<div>
<h3 class="text-sm font-medium mb-3">Packet Statistics</h3>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Total Packets:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Total Packets:</span>
<span class="text-sm font-mono">
{AprsmeWeb.StatusLive.Index.format_number(@aprs_status.packet_stats.total_packets)}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Packets/Sec:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Packets/Sec:</span>
<span class="text-sm font-mono">
{@aprs_status.packet_stats.packets_per_second}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Packets/Min:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Packets/Min:</span>
<span class="text-sm font-mono">
{@aprs_status.packet_stats.packets_per_second * 60}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Last Packet:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Last Packet:</span>
<span class="text-sm">
<%= if @aprs_status.packet_stats.last_packet_at do %>
{AprsmeWeb.StatusLive.Index.format_time_ago(@aprs_status.packet_stats.last_packet_at)}
<% else %>
<span class="opacity-50">None</span>
<span class="text-gray-400 dark:text-gray-500">None</span>
<% end %>
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">Stored Packets:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Stored Packets:</span>
<span class="text-sm font-mono">
{AprsmeWeb.StatusLive.Index.format_number(@aprs_status.stored_packet_count)}
</span>
@ -113,19 +117,24 @@ defmodule AprsmeWeb.StatusLive.ConnectionCardComponent do
</div>
</div>
<div class="divider"></div>
<div class="border-t border-gray-200 dark:border-white/10 my-4"></div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium opacity-70">Connection Health:</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Connection Health:</span>
<div class="flex items-center">
<div class="rating rating-sm mr-2">
<div class="flex items-center mr-2">
<%= for i <- 1..5 do %>
<input
type="radio"
name="health-rating"
class="mask mask-star-2 bg-orange-400"
disabled
checked={i <= @health_score}
/>
<svg
class={"h-4 w-4 #{if i <= @health_score, do: "text-amber-400", else: "text-gray-300 dark:text-gray-600"}"}
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z"
clip-rule="evenodd"
/>
</svg>
<% end %>
</div>
<span class="text-sm font-medium">
@ -133,7 +142,7 @@ defmodule AprsmeWeb.StatusLive.ConnectionCardComponent do
</span>
</div>
</div>
<p class="text-xs opacity-70 mt-1">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{AprsmeWeb.StatusLive.Index.get_health_description(@health_score, @aprs_status.connected)}
</p>
</div>

View file

@ -101,38 +101,41 @@ defmodule AprsmeWeb.StatusLive.Index do
@impl true
def render(assigns) do
~H"""
<div class="container mx-auto max-w-4xl p-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="mx-auto max-w-4xl p-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">
<div class="px-6 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="card-title text-3xl">{gettext("APRS.me System Status")}</h1>
<div class="text-sm opacity-70">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{gettext("APRS.me System Status")}</h1>
<div class="text-sm text-gray-500 dark:text-gray-400">
{gettext("Last updated:")}
<span class="font-mono">{Calendar.strftime(@current_time, "%H:%M:%S UTC")}</span>
</div>
</div>
<!-- Overall Status Alert -->
<%= if not @aprs_status.connected do %>
<div class="alert alert-error mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<h3 class="font-bold">{gettext("APRS-IS Connection Issue")}</h3>
<div class="text-xs">
{gettext(
"The system is currently disconnected from the APRS-IS network. This may be due to network issues or server maintenance."
)}
<div class="rounded-md bg-red-50 p-4 mb-6 dark:bg-red-500/10">
<div class="flex">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-red-400 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-300">{gettext("APRS-IS Connection Issue")}</h3>
<div class="mt-1 text-sm text-red-700 dark:text-red-400">
{gettext(
"The system is currently disconnected from the APRS-IS network. This may be due to network issues or server maintenance."
)}
</div>
</div>
</div>
</div>
@ -140,239 +143,254 @@ defmodule AprsmeWeb.StatusLive.Index do
<!-- APRS-IS Connection Status -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">{gettext("APRS-IS Connection")}</h2>
<div class="card bg-base-200">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Status:")}</span>
<%= if @aprs_status.connected do %>
<div class="badge badge-success gap-1">
<div class="w-2 h-2 bg-current rounded-full"></div>
{gettext("Connected")}
</div>
<h2 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">{gettext("APRS-IS Connection")}</h2>
<div class="rounded-lg bg-gray-50 p-6 dark:bg-gray-700/50">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Status:")}</span>
<%= if @aprs_status.connected do %>
<span class="inline-flex items-center gap-1 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="h-2 w-2 rounded-full bg-current"></span>
{gettext("Connected")}
</span>
<% else %>
<span class="inline-flex items-center gap-1 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="h-2 w-2 rounded-full bg-current"></span>
{gettext("Disconnected")}
</span>
<% end %>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Server:")}</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{@aprs_status.server}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Login ID:")}</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">{@aprs_status.login_id}</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Connected Since:")}
</span>
<span class="text-sm text-gray-900 dark:text-white">
<%= if @aprs_status.connected_at do %>
{Calendar.strftime(@aprs_status.connected_at, "%Y-%m-%d %H:%M:%S UTC")}
<% else %>
<div class="badge badge-error gap-1">
<div class="w-2 h-2 bg-current rounded-full"></div>
{gettext("Disconnected")}
</div>
<span class="text-gray-400 dark:text-gray-500">{gettext("Not connected")}</span>
<% end %>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Server:")}</span>
<span class="text-sm font-mono">
{@aprs_status.server}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Login ID:")}</span>
<span class="text-sm font-mono">{@aprs_status.login_id}</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">
{gettext("Connected Since:")}
</span>
<span class="text-sm">
<%= if @aprs_status.connected_at do %>
{Calendar.strftime(@aprs_status.connected_at, "%Y-%m-%d %H:%M:%S UTC")}
<% else %>
<span class="opacity-50">{gettext("Not connected")}</span>
<% end %>
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Uptime:")}</span>
<span class={[
"text-sm font-medium",
if(@aprs_status.connected, do: "text-success", else: "text-error")
]}>
{format_uptime(@aprs_status.uptime_seconds)}
</span>
</div>
<div class="flex items-center col-span-2">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Filter:")}</span>
<div class="badge badge-outline font-mono">
{@aprs_status.filter}
</div>
</div>
</span>
</div>
<!-- Packet Statistics -->
<div class="divider"></div>
<div>
<h3 class="text-sm font-medium mb-3">{gettext("Packet Statistics")}</h3>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">
{gettext("Total Packets:")}
</span>
<span class="text-sm font-mono">
{format_number(@aprs_status.packet_stats.total_packets)}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">
{gettext("Packets/Sec:")}
</span>
<span class="text-sm font-mono">
{@aprs_status.packet_stats.packets_per_second}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">
{gettext("Packets/Min:")}
</span>
<span class="text-sm font-mono">
{@aprs_status.packet_stats.packets_per_second * 60}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">
{gettext("Last Packet:")}
</span>
<span class="text-sm">
<%= if @aprs_status.packet_stats.last_packet_at do %>
{format_time_ago(@aprs_status.packet_stats.last_packet_at)}
<% else %>
<span class="opacity-50">{gettext("None")}</span>
<% end %>
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">
{gettext("Stored Packets:")}
</span>
<span class="text-sm font-mono">
{format_number(@aprs_status.stored_packet_count)}
</span>
</div>
</div>
<div class="mt-2">
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">
{gettext("Oldest Packet:")}
</span>
<span class="text-sm">
<%= if @aprs_status[:oldest_packet_timestamp] do %>
<span class="font-mono">
{Calendar.strftime(
@aprs_status.oldest_packet_timestamp,
"%Y-%m-%d %H:%M:%S UTC"
)}
</span>
<span class="opacity-70">
({format_time_ago(@aprs_status.oldest_packet_timestamp)})
</span>
<% else %>
<span class="opacity-50">{gettext("No packets stored")}</span>
<% end %>
</span>
</div>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Uptime:")}</span>
<span class={[
"text-sm font-medium",
if(@aprs_status.connected,
do: "text-green-600 dark:text-green-400",
else: "text-red-600 dark:text-red-400"
)
]}>
{format_uptime(@aprs_status.uptime_seconds)}
</span>
</div>
<!-- Health Score -->
<div class="divider"></div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium opacity-70">{gettext("Connection Health:")}</span>
<div class="flex items-center">
<div class="rating rating-sm mr-2">
<%= for i <- 1..5 do %>
<input
type="radio"
name="health-rating"
class="mask mask-star-2 bg-orange-400"
disabled
checked={i <= @health_score}
/>
<% end %>
</div>
<span class="text-sm font-medium">
{@health_score}/5
</span>
</div>
<div class="flex items-center col-span-2">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Filter:")}</span>
<span class="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium font-mono text-gray-600 dark:bg-gray-400/10 dark:text-gray-400">
{@aprs_status.filter}
</span>
</div>
<p class="text-xs opacity-70 mt-1">
{get_health_description(@health_score, @aprs_status.connected)}
</p>
</div>
<!-- Packet Statistics -->
<div class="border-t border-gray-200 dark:border-white/10 my-4"></div>
<div>
<h3 class="text-sm font-medium mb-3 text-gray-900 dark:text-white">{gettext("Packet Statistics")}</h3>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Total Packets:")}
</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{format_number(@aprs_status.packet_stats.total_packets)}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Packets/Sec:")}
</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{@aprs_status.packet_stats.packets_per_second}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Packets/Min:")}
</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{@aprs_status.packet_stats.packets_per_second * 60}
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Last Packet:")}
</span>
<span class="text-sm text-gray-900 dark:text-white">
<%= if @aprs_status.packet_stats.last_packet_at do %>
{format_time_ago(@aprs_status.packet_stats.last_packet_at)}
<% else %>
<span class="text-gray-400 dark:text-gray-500">{gettext("None")}</span>
<% end %>
</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Stored Packets:")}
</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{format_number(@aprs_status.stored_packet_count)}
</span>
</div>
</div>
<div class="mt-2">
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Oldest Packet:")}
</span>
<span class="text-sm text-gray-900 dark:text-white">
<%= if @aprs_status[:oldest_packet_timestamp] do %>
<span class="font-mono">
{Calendar.strftime(
@aprs_status.oldest_packet_timestamp,
"%Y-%m-%d %H:%M:%S UTC"
)}
</span>
<span class="text-gray-500 dark:text-gray-400">
({format_time_ago(@aprs_status.oldest_packet_timestamp)})
</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500">{gettext("No packets stored")}</span>
<% end %>
</span>
</div>
</div>
</div>
<!-- Health Score -->
<div class="border-t border-gray-200 dark:border-white/10 my-4"></div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{gettext("Connection Health:")}</span>
<div class="flex items-center gap-1">
<%= for i <- 1..5 do %>
<svg
class={[
"h-5 w-5",
if(i <= @health_score, do: "text-amber-400", else: "text-gray-300 dark:text-gray-600")
]}
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z"
clip-rule="evenodd"
/>
</svg>
<% end %>
<span class="ml-1 text-sm font-medium text-gray-900 dark:text-white">
{@health_score}/5
</span>
</div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{get_health_description(@health_score, @aprs_status.connected)}
</p>
</div>
</div>
<!-- Cluster Information (if available) -->
<%= if Map.has_key?(@aprs_status, :cluster_info) do %>
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">{gettext("Cluster Status")}</h2>
<div class="card bg-base-200">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Total Nodes:")}</span>
<span class="text-sm font-mono">{@aprs_status.cluster_info.total_nodes}</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Connected Nodes:")}</span>
<span class="text-sm font-mono">{@aprs_status.cluster_info.connected_nodes}</span>
</div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Leader Node:")}</span>
<span class="text-sm font-mono">{@aprs_status.cluster_info.leader_node}</span>
</div>
<h2 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">{gettext("Cluster Status")}</h2>
<div class="rounded-lg bg-gray-50 p-6 dark:bg-gray-700/50">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Total Nodes:")}</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{@aprs_status.cluster_info.total_nodes}
</span>
</div>
<div class="divider"></div>
<div class="flex items-center">
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Cluster Mode:")}</span>
<div class="badge badge-success gap-1">
<div class="w-2 h-2 bg-current rounded-full"></div>
{gettext("Enabled")}
</div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
{gettext("Connected Nodes:")}
</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{@aprs_status.cluster_info.connected_nodes}
</span>
</div>
<%= if Map.has_key?(@aprs_status.cluster_info, :all_nodes) do %>
<div class="mt-4">
<span class="text-sm font-medium opacity-70">{gettext("Cluster Nodes:")}</span>
<div class="flex flex-wrap gap-2 mt-2">
<%= for node_name <- @aprs_status.cluster_info.all_nodes do %>
<div class={[
"badge gap-1",
if(node_name == @aprs_status.cluster_info.leader_node,
do: "badge-primary",
else: "badge-outline"
)
]}>
<%= if node_name == @aprs_status.cluster_info.leader_node do %>
<div class="w-2 h-2 bg-current rounded-full"></div>
<% end %>
<span class="font-mono text-xs">{node_name}</span>
</div>
<% end %>
</div>
<p class="text-xs opacity-70 mt-1">
{gettext("Leader node")} <span class="badge badge-primary badge-xs">{gettext("highlighted")}</span>
</p>
</div>
<% end %>
<p class="text-xs opacity-70 mt-2">
{gettext(
"Status is aggregated from all nodes in the cluster. Only the leader node maintains the APRS-IS connection."
)}
</p>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Leader Node:")}</span>
<span class="text-sm font-mono text-gray-900 dark:text-white">
{@aprs_status.cluster_info.leader_node}
</span>
</div>
</div>
<div class="border-t border-gray-200 dark:border-white/10 my-4"></div>
<div class="flex items-center">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Cluster Mode:")}</span>
<span class="inline-flex items-center gap-1 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="h-2 w-2 rounded-full bg-current"></span>
{gettext("Enabled")}
</span>
</div>
<%= if Map.has_key?(@aprs_status.cluster_info, :all_nodes) do %>
<div class="mt-4">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{gettext("Cluster Nodes:")}</span>
<div class="flex flex-wrap gap-2 mt-2">
<%= for node_name <- @aprs_status.cluster_info.all_nodes do %>
<span class={[
"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium font-mono",
if(node_name == @aprs_status.cluster_info.leader_node,
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-400/10 dark:text-indigo-400",
else: "bg-gray-100 text-gray-600 dark:bg-gray-400/10 dark:text-gray-400"
)
]}>
<%= if node_name == @aprs_status.cluster_info.leader_node do %>
<span class="h-2 w-2 rounded-full bg-current"></span>
<% end %>
{node_name}
</span>
<% end %>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{gettext("Leader node")}
<span class="inline-flex items-center rounded-md bg-indigo-100 px-1.5 py-0.5 text-xs font-medium text-indigo-700 dark:bg-indigo-400/10 dark:text-indigo-400">
{gettext("highlighted")}
</span>
</p>
</div>
<% end %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
{gettext(
"Status is aggregated from all nodes in the cluster. Only the leader node maintains the APRS-IS connection."
)}
</p>
</div>
</div>
<% end %>

View file

@ -6,51 +6,66 @@ defmodule AprsmeWeb.UserConfirmationInstructionsLive do
def render(assigns) do
~H"""
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h1 class="card-title text-3xl mb-4 justify-center">
Resend confirmation instructions
</h1>
<p class="text-base-content/70 mb-6">
Enter your email address to receive a new confirmation link
</p>
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
Resend confirmation instructions
</h2>
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
Enter your email address to receive a new confirmation link
</p>
</div>
<.simple_form :let={_f} for={%{}} as={:user} id="resend_confirmation_form" phx-submit="send_instructions">
<div class="form-control w-full">
<label class="label">
<span class="label-text">Email</span>
</label>
<input
type="email"
name="user[email]"
class="input input-bordered w-full bg-base-100 text-base-content"
placeholder="Enter your email"
required
/>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Sending...">
Resend confirmation instructions
</button>
</div>
</.simple_form>
<div class="divider">OR</div>
<div class="text-center text-sm">
<.link navigate={~p"/users/register"} class="link link-primary">
Register
</.link>
|
<.link navigate={~p"/users/log_in"} class="link link-primary">
Log in
</.link>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div class="bg-white px-6 py-12 shadow-sm sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<.simple_form :let={_f} for={%{}} as={:user} id="resend_confirmation_form" phx-submit="send_instructions">
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Email</label>
<div class="mt-2">
<input
type="email"
name="user[email]"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
placeholder="Enter your email"
required
/>
</div>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with="Sending..."
>
Resend confirmation instructions
</button>
</div>
</.simple_form>
<div class="relative mt-10">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-200 dark:border-white/10"></div>
</div>
<div class="relative flex justify-center text-sm/6">
<span class="bg-white px-6 text-gray-900 dark:bg-gray-800/50 dark:text-white">OR</span>
</div>
</div>
<div class="mt-6 text-center text-sm/6 text-gray-500 dark:text-gray-400">
<.link
navigate={~p"/users/register"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Register
</.link>
<span class="mx-2">|</span>
<.link
navigate={~p"/users/log_in"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Log in
</.link>
</div>
</div>
</div>

View file

@ -6,36 +6,55 @@ defmodule AprsmeWeb.UserConfirmationLive do
def render(%{live_action: :edit} = assigns) do
~H"""
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h1 class="card-title text-3xl mb-4 justify-center">Confirm your account</h1>
<p class="text-base-content/70 mb-6">Click the button below to confirm your account</p>
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
Confirm your account
</h2>
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
Click the button below to confirm your account
</p>
</div>
<.simple_form :let={_f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account">
<input type="hidden" name="user[token]" value={@token} />
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div class="bg-white px-6 py-12 shadow-sm sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<.simple_form :let={_f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account">
<input type="hidden" name="user[token]" value={@token} />
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Confirming...">
Confirm my account
</button>
</div>
</.simple_form>
<div class="divider">OR</div>
<div class="text-center text-sm">
<.link navigate={~p"/users/register"} class="link link-primary">
Register
</.link>
|
<.link navigate={~p"/users/log_in"} class="link link-primary">
Log in
</.link>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with="Confirming..."
>
Confirm my account
</button>
</div>
</.simple_form>
<div class="relative mt-10">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-200 dark:border-white/10"></div>
</div>
<div class="relative flex justify-center text-sm/6">
<span class="bg-white px-6 text-gray-900 dark:bg-gray-800/50 dark:text-white">OR</span>
</div>
</div>
<div class="mt-6 text-center text-sm/6 text-gray-500 dark:text-gray-400">
<.link
navigate={~p"/users/register"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Register
</.link>
<span class="mx-2">|</span>
<.link
navigate={~p"/users/log_in"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Log in
</.link>
</div>
</div>
</div>

View file

@ -6,47 +6,66 @@ defmodule AprsmeWeb.UserForgotPasswordLive do
def render(assigns) do
~H"""
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h1 class="card-title text-3xl mb-4 justify-center">Forgot your password?</h1>
<p class="text-base-content/70 mb-6">We'll send a password reset link to your inbox</p>
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
Forgot your password?
</h2>
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
We'll send a password reset link to your inbox
</p>
</div>
<.simple_form :let={_f} id="reset_password_form" for={%{}} as={:user} phx-submit="send_email">
<div class="form-control w-full">
<label class="label">
<span class="label-text">Email</span>
</label>
<input
type="email"
name="user[email]"
class="input input-bordered w-full bg-base-100 text-base-content"
placeholder="Enter your email"
required
/>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Sending...">
Send password reset instructions
</button>
</div>
</.simple_form>
<div class="divider">OR</div>
<div class="text-center text-sm">
<.link navigate={~p"/users/register"} class="link link-primary">
Register
</.link>
|
<.link navigate={~p"/users/log_in"} class="link link-primary">
Log in
</.link>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div class="bg-white px-6 py-12 shadow-sm sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<.simple_form :let={_f} id="reset_password_form" for={%{}} as={:user} phx-submit="send_email">
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Email</label>
<div class="mt-2">
<input
type="email"
name="user[email]"
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
placeholder="Enter your email"
required
/>
</div>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with="Sending..."
>
Send password reset instructions
</button>
</div>
</.simple_form>
<div class="relative mt-10">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-200 dark:border-white/10"></div>
</div>
<div class="relative flex justify-center text-sm/6">
<span class="bg-white px-6 text-gray-900 dark:bg-gray-800/50 dark:text-white">OR</span>
</div>
</div>
<div class="mt-6 text-center text-sm/6 text-gray-500 dark:text-gray-400">
<.link
navigate={~p"/users/register"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Register
</.link>
<span class="mx-2">|</span>
<.link
navigate={~p"/users/log_in"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Log in
</.link>
</div>
</div>
</div>

View file

@ -7,82 +7,100 @@ defmodule AprsmeWeb.UserLoginLive do
def render(assigns) do
~H"""
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h1 class="card-title text-3xl mb-4 justify-center">{gettext("Sign in to account")}</h1>
<p class="text-base-content/70 mb-6">
{gettext("Don't have an account?")}
<.link navigate={~p"/users/register"} class="link link-primary">
{gettext("Sign up")}
</.link>
{gettext("for an account now.")}
</p>
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
{gettext("Sign in to account")}
</h2>
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
{gettext("Don't have an account?")}
<.link
navigate={~p"/users/register"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{gettext("Sign up")}
</.link>
{gettext("for an account now.")}
</p>
</div>
<.simple_form
:let={f}
id="login_form"
for={@changeset}
action={~p"/users/log_in"}
as={:user}
phx-update="ignore"
>
<div class="form-control w-full">
<label class="label">
<span class="label-text">{gettext("Email")}</span>
</label>
<input
type="email"
name={Phoenix.HTML.Form.input_name(f, :email)}
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
class="input input-bordered w-full"
placeholder={gettext("Enter your email")}
required
/>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">{gettext("Password")}</span>
</label>
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password)}
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
class="input input-bordered w-full"
placeholder={gettext("Enter your password")}
required
/>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start">
<input
type="checkbox"
name={Phoenix.HTML.Form.input_name(f, :remember_me)}
value="true"
class="checkbox checkbox-primary mr-2"
/>
<span class="label-text">{gettext("Keep me logged in")}</span>
</label>
</div>
<div class="text-center mb-4">
<.link navigate={~p"/users/reset_password"} class="link link-primary text-sm">
{gettext("Forgot your password?")}
</.link>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with={gettext("Signing in...")}>
{gettext("Sign in →")}
</button>
</div>
</.simple_form>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div class="bg-white px-6 py-12 shadow-sm sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<.simple_form
:let={f}
id="login_form"
for={@changeset}
action={~p"/users/log_in"}
as={:user}
phx-update="ignore"
>
<div>
<label for="user_email" class="block text-sm/6 font-medium text-gray-900 dark:text-white">
{gettext("Email")}
</label>
<div class="mt-2">
<input
type="email"
id="user_email"
name={Phoenix.HTML.Form.input_name(f, :email)}
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
placeholder={gettext("Enter your email")}
required
/>
</div>
</div>
</div>
<div>
<label for="user_password" class="block text-sm/6 font-medium text-gray-900 dark:text-white">
{gettext("Password")}
</label>
<div class="mt-2">
<input
type="password"
id="user_password"
name={Phoenix.HTML.Form.input_name(f, :password)}
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
placeholder={gettext("Enter your password")}
required
/>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input
type="checkbox"
id="user_remember_me"
name={Phoenix.HTML.Form.input_name(f, :remember_me)}
value="true"
class="size-4 rounded-sm border-gray-300 text-indigo-600 focus:ring-indigo-600 dark:border-white/10 dark:bg-white/5"
/>
<label for="user_remember_me" class="block text-sm/6 text-gray-900 dark:text-white">
{gettext("Keep me logged in")}
</label>
</div>
<div class="text-sm/6">
<.link
navigate={~p"/users/reset_password"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{gettext("Forgot your password?")}
</.link>
</div>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with={gettext("Signing in...")}
>
{gettext("Sign in")}
</button>
</div>
</.simple_form>
</div>
</div>
</div>

View file

@ -9,109 +9,130 @@ defmodule AprsmeWeb.UserRegistrationLive do
def render(assigns) do
~H"""
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h1 class="card-title text-3xl mb-4 justify-center">{gettext("Register for an account")}</h1>
<p class="text-base-content/70 mb-6">
{gettext("Already registered?")}
<.link navigate={~p"/users/log_in"} class="link link-primary">
{gettext("Sign in")}
</.link>
{gettext("to your account now.")}
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
{gettext("Register for an account")}
</h2>
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
{gettext("Already registered?")}
<.link
navigate={~p"/users/log_in"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{gettext("Sign in")}
</.link>
{gettext("to your account now.")}
</p>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div class="bg-white px-6 py-12 shadow-sm sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<.simple_form
:let={f}
id="registration_form"
for={@changeset}
phx-submit="save"
phx-change="validate"
phx-trigger-action={@trigger_submit}
action={~p"/users/log_in?_action=registered"}
method="post"
as={:user}
>
<div :if={@changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
<p class="text-sm text-red-700 dark:text-red-400">
{gettext("Oops, something went wrong! Please check the errors below.")}
</p>
<.simple_form
:let={f}
id="registration_form"
for={@changeset}
phx-submit="save"
phx-change="validate"
phx-trigger-action={@trigger_submit}
action={~p"/users/log_in?_action=registered"}
method="post"
as={:user}
>
<div :if={@changeset.action == :insert} class="alert alert-error mb-4">
<span>{gettext("Oops, something went wrong! Please check the errors below.")}</span>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">{gettext("Email")}</span>
</label>
<input
type="email"
name={Phoenix.HTML.Form.input_name(f, :email)}
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
@changeset.action && Keyword.has_key?(@changeset.errors, :email) && "input-error"
]}
placeholder={gettext("Enter your email")}
required
/>
<label :if={@changeset.action && Keyword.has_key?(@changeset.errors, :email)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@changeset.errors, :email))}
</span>
</label>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">{gettext("Callsign")}</span>
</label>
<input
type="text"
name={Phoenix.HTML.Form.input_name(f, :callsign)}
value={Phoenix.HTML.Form.input_value(f, :callsign) || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
@changeset.action && Keyword.has_key?(@changeset.errors, :callsign) && "input-error"
]}
placeholder={gettext("Enter your amateur radio callsign")}
required
/>
<label :if={@changeset.action && Keyword.has_key?(@changeset.errors, :callsign)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@changeset.errors, :callsign))}
</span>
</label>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">{gettext("Password")}</span>
</label>
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password)}
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
@changeset.action && Keyword.has_key?(@changeset.errors, :password) && "input-error"
]}
placeholder={gettext("Enter your password")}
required
/>
<label :if={@changeset.action && Keyword.has_key?(@changeset.errors, :password)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@changeset.errors, :password))}
</span>
</label>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with={gettext("Creating account...")}>
{gettext("Create an account")}
</button>
</div>
</.simple_form>
</div>
</div>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">{gettext("Email")}</label>
<div class="mt-2">
<input
type="email"
name={Phoenix.HTML.Form.input_name(f, :email)}
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
class={[
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(@changeset.action && Keyword.has_key?(@changeset.errors, :email),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder={gettext("Enter your email")}
required
/>
</div>
<p
:if={@changeset.action && Keyword.has_key?(@changeset.errors, :email)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@changeset.errors, :email))}
</p>
</div>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">{gettext("Callsign")}</label>
<div class="mt-2">
<input
type="text"
name={Phoenix.HTML.Form.input_name(f, :callsign)}
value={Phoenix.HTML.Form.input_value(f, :callsign) || ""}
class={[
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(@changeset.action && Keyword.has_key?(@changeset.errors, :callsign),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder={gettext("Enter your amateur radio callsign")}
required
/>
</div>
<p
:if={@changeset.action && Keyword.has_key?(@changeset.errors, :callsign)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@changeset.errors, :callsign))}
</p>
</div>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">{gettext("Password")}</label>
<div class="mt-2">
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password)}
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
class={[
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(@changeset.action && Keyword.has_key?(@changeset.errors, :password),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder={gettext("Enter your password")}
required
/>
</div>
<p
:if={@changeset.action && Keyword.has_key?(@changeset.errors, :password)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@changeset.errors, :password))}
</p>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with={gettext("Creating account...")}
>
{gettext("Create an account")}
</button>
</div>
</.simple_form>
</div>
</div>
</div>

View file

@ -6,72 +6,93 @@ defmodule AprsmeWeb.UserResetPasswordLive do
def render(assigns) do
~H"""
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h1 class="card-title text-3xl mb-4 justify-center">Reset Password</h1>
<p class="text-base-content/70 mb-6">Enter your new password below</p>
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
Reset Password
</h2>
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
Enter your new password below
</p>
</div>
<.simple_form
:let={f}
for={@changeset}
id="reset_password_form"
phx-submit="reset_password"
phx-change="validate"
>
<div :if={@changeset.action == :insert} class="alert alert-error mb-4">
<span>Oops, something went wrong! Please check the errors below.</span>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div class="bg-white px-6 py-12 shadow-sm sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<.simple_form
:let={f}
for={@changeset}
id="reset_password_form"
phx-submit="reset_password"
phx-change="validate"
>
<div :if={@changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
<p class="text-sm text-red-700 dark:text-red-400">
Oops, something went wrong! Please check the errors below.
</p>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">New password</span>
</label>
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password)}
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
class="input input-bordered w-full bg-base-100 text-base-content"
placeholder="Enter new password"
required
/>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Confirm new password</span>
</label>
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password_confirmation)}
value={Phoenix.HTML.Form.input_value(f, :password_confirmation) || ""}
class="input input-bordered w-full bg-base-100 text-base-content"
placeholder="Confirm new password"
required
/>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Resetting...">
Reset Password
</button>
</div>
</.simple_form>
<div class="divider">OR</div>
<div class="text-center text-sm">
<.link navigate={~p"/users/register"} class="link link-primary">
Register
</.link>
|
<.link navigate={~p"/users/log_in"} class="link link-primary">
Log in
</.link>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">New password</label>
<div class="mt-2">
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password)}
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
placeholder="Enter new password"
required
/>
</div>
</div>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Confirm new password</label>
<div class="mt-2">
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password_confirmation)}
value={Phoenix.HTML.Form.input_value(f, :password_confirmation) || ""}
class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
placeholder="Confirm new password"
required
/>
</div>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with="Resetting..."
>
Reset Password
</button>
</div>
</.simple_form>
<div class="relative mt-10">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-200 dark:border-white/10"></div>
</div>
<div class="relative flex justify-center text-sm/6">
<span class="bg-white px-6 text-gray-900 dark:bg-gray-800/50 dark:text-white">OR</span>
</div>
</div>
<div class="mt-6 text-center text-sm/6 text-gray-500 dark:text-gray-400">
<.link
navigate={~p"/users/register"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Register
</.link>
<span class="mx-2">|</span>
<.link
navigate={~p"/users/log_in"}
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Log in
</.link>
</div>
</div>
</div>

View file

@ -8,250 +8,289 @@ defmodule AprsmeWeb.UserSettingsLive do
def render(assigns) do
~H"""
<div class="hero min-h-screen bg-base-200">
<div class="hero-content flex-col w-full max-w-4xl">
<div class="min-h-screen bg-gray-50 py-12 dark:bg-gray-900">
<div class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold">Account Settings</h1>
<p class="text-base-content/70">Update your email address, callsign, and password</p>
<h1 class="text-4xl font-bold text-gray-900 dark:text-white">Account Settings</h1>
<p class="mt-2 text-gray-500 dark:text-gray-400">Update your email address, callsign, and password</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 w-full">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Change Email Section -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Change Email</h2>
<div class="bg-white px-6 py-8 shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<h2 class="text-xl font-semibold text-gray-900 mb-6 dark:text-white">Change Email</h2>
<.simple_form
:let={f}
id="email_form"
for={@email_changeset}
phx-submit="update_email"
phx-change="validate_email"
>
<div :if={@email_changeset.action == :insert} class="alert alert-error mb-4">
<span>Oops, something went wrong! Please check the errors below.</span>
</div>
<.simple_form
:let={f}
id="email_form"
for={@email_changeset}
phx-submit="update_email"
phx-change="validate_email"
>
<div :if={@email_changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
<p class="text-sm text-red-700 dark:text-red-400">
Oops, something went wrong! Please check the errors below.
</p>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Email</span>
</label>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Email</label>
<div class="mt-2">
<input
type="email"
name={Phoenix.HTML.Form.input_name(f, :email)}
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
Keyword.has_key?(@email_changeset.errors, :email) && "input-error"
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(Keyword.has_key?(@email_changeset.errors, :email),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder="Enter your email"
required
/>
<label :if={Keyword.has_key?(@email_changeset.errors, :email)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@email_changeset.errors, :email))}
</span>
</label>
</div>
<p
:if={Keyword.has_key?(@email_changeset.errors, :email)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@email_changeset.errors, :email))}
</p>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Current password</span>
</label>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Current password</label>
<div class="mt-2">
<input
type="password"
name="current_password"
id="current_password_for_email"
value={@email_form_current_password || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
Keyword.has_key?(@email_changeset.errors, :current_password) && "input-error"
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(Keyword.has_key?(@email_changeset.errors, :current_password),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder="Enter current password"
required
/>
<label :if={Keyword.has_key?(@email_changeset.errors, :current_password)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@email_changeset.errors, :current_password))}
</span>
</label>
</div>
<p
:if={Keyword.has_key?(@email_changeset.errors, :current_password)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@email_changeset.errors, :current_password))}
</p>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Changing...">
Change Email
</button>
</div>
</.simple_form>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with="Changing..."
>
Change Email
</button>
</div>
</.simple_form>
</div>
<!-- Change Callsign Section -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Change Callsign</h2>
<div class="bg-white px-6 py-8 shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<h2 class="text-xl font-semibold text-gray-900 mb-6 dark:text-white">Change Callsign</h2>
<.simple_form
:let={f}
id="callsign_form"
for={@callsign_changeset}
phx-submit="update_callsign"
phx-change="validate_callsign"
>
<div :if={@callsign_changeset.action == :insert} class="alert alert-error mb-4">
<span>Oops, something went wrong! Please check the errors below.</span>
</div>
<.simple_form
:let={f}
id="callsign_form"
for={@callsign_changeset}
phx-submit="update_callsign"
phx-change="validate_callsign"
>
<div :if={@callsign_changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
<p class="text-sm text-red-700 dark:text-red-400">
Oops, something went wrong! Please check the errors below.
</p>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Callsign</span>
</label>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Callsign</label>
<div class="mt-2">
<input
type="text"
name={Phoenix.HTML.Form.input_name(f, :callsign)}
value={Phoenix.HTML.Form.input_value(f, :callsign) || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
Keyword.has_key?(@callsign_changeset.errors, :callsign) && "input-error"
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(Keyword.has_key?(@callsign_changeset.errors, :callsign),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder="Enter your callsign"
required
/>
<label :if={Keyword.has_key?(@callsign_changeset.errors, :callsign)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@callsign_changeset.errors, :callsign))}
</span>
</label>
</div>
<p
:if={Keyword.has_key?(@callsign_changeset.errors, :callsign)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@callsign_changeset.errors, :callsign))}
</p>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Current password</span>
</label>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Current password</label>
<div class="mt-2">
<input
type="password"
name="current_password"
id="current_password_for_callsign"
value={@callsign_form_current_password || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
Keyword.has_key?(@callsign_changeset.errors, :current_password) && "input-error"
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(Keyword.has_key?(@callsign_changeset.errors, :current_password),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder="Enter current password"
required
/>
<label :if={Keyword.has_key?(@callsign_changeset.errors, :current_password)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@callsign_changeset.errors, :current_password))}
</span>
</label>
</div>
<p
:if={Keyword.has_key?(@callsign_changeset.errors, :current_password)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@callsign_changeset.errors, :current_password))}
</p>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Changing...">
Change Callsign
</button>
</div>
</.simple_form>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with="Changing..."
>
Change Callsign
</button>
</div>
</.simple_form>
</div>
<!-- Change Password Section -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Change Password</h2>
<div class="bg-white px-6 py-8 shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
<h2 class="text-xl font-semibold text-gray-900 mb-6 dark:text-white">Change Password</h2>
<.simple_form
:let={f}
id="password_form"
for={@password_changeset}
action={~p"/users/log_in?_action=password_updated"}
method="post"
phx-change="validate_password"
phx-submit="update_password"
phx-trigger-action={@trigger_submit}
>
<div :if={@password_changeset.action == :insert} class="alert alert-error mb-4">
<span>Oops, something went wrong! Please check the errors below.</span>
</div>
<.simple_form
:let={f}
id="password_form"
for={@password_changeset}
action={~p"/users/log_in?_action=password_updated"}
method="post"
phx-change="validate_password"
phx-submit="update_password"
phx-trigger-action={@trigger_submit}
>
<div :if={@password_changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
<p class="text-sm text-red-700 dark:text-red-400">
Oops, something went wrong! Please check the errors below.
</p>
</div>
<input type="hidden" name={Phoenix.HTML.Form.input_name(f, :email)} value={@current_email} />
<input type="hidden" name={Phoenix.HTML.Form.input_name(f, :email)} value={@current_email} />
<div class="form-control w-full">
<label class="label">
<span class="label-text">New password</span>
</label>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">New password</label>
<div class="mt-2">
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password)}
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
Keyword.has_key?(@password_changeset.errors, :password) && "input-error"
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(Keyword.has_key?(@password_changeset.errors, :password),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder="Enter new password"
required
/>
<label :if={Keyword.has_key?(@password_changeset.errors, :password)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@password_changeset.errors, :password))}
</span>
</label>
</div>
<p
:if={Keyword.has_key?(@password_changeset.errors, :password)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@password_changeset.errors, :password))}
</p>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Confirm new password</span>
</label>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Confirm new password</label>
<div class="mt-2">
<input
type="password"
name={Phoenix.HTML.Form.input_name(f, :password_confirmation)}
value={Phoenix.HTML.Form.input_value(f, :password_confirmation) || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
Keyword.has_key?(@password_changeset.errors, :password_confirmation) &&
"input-error"
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(Keyword.has_key?(@password_changeset.errors, :password_confirmation),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder="Confirm new password"
required
/>
<label :if={Keyword.has_key?(@password_changeset.errors, :password_confirmation)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@password_changeset.errors, :password_confirmation))}
</span>
</label>
</div>
<p
:if={Keyword.has_key?(@password_changeset.errors, :password_confirmation)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@password_changeset.errors, :password_confirmation))}
</p>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Current password</span>
</label>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Current password</label>
<div class="mt-2">
<input
type="password"
name="current_password"
id="current_password_for_password"
value={@current_password || ""}
class={[
"input input-bordered w-full bg-base-100 text-base-content",
Keyword.has_key?(@password_changeset.errors, :current_password) && "input-error"
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500 dark:focus:outline-indigo-500",
if(Keyword.has_key?(@password_changeset.errors, :current_password),
do: "outline-red-500 dark:outline-red-400",
else: "outline-gray-300 dark:outline-white/10"
)
]}
placeholder="Enter current password"
required
/>
<label :if={Keyword.has_key?(@password_changeset.errors, :current_password)} class="label">
<span class="label-text-alt text-error">
{translate_error(Keyword.get(@password_changeset.errors, :current_password))}
</span>
</label>
</div>
<p
:if={Keyword.has_key?(@password_changeset.errors, :current_password)}
class="mt-1 text-sm text-red-600 dark:text-red-400"
>
{translate_error(Keyword.get(@password_changeset.errors, :current_password))}
</p>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Changing...">
Change Password
</button>
</div>
</.simple_form>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
phx-disable-with="Changing..."
>
Change Password
</button>
</div>
</.simple_form>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<%= if @weather_packet do %>
<div class="bg-base-200">
<div class="bg-base-100 shadow-sm">
<div class="bg-gray-50 dark:bg-gray-900">
<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="flex items-center">
@ -8,7 +8,7 @@
{gettext("Weather Station")}
</h1>
<div class="ml-3 flex items-center space-x-3">
<span class="badge badge-primary">
<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>
<% {symbol_table_id, symbol_code} =
@ -39,13 +39,22 @@
</div>
<div class="mt-3 flex md:ml-4 md:mt-0">
<div class="flex space-x-2">
<.link navigate={~p"/info/#{@callsign}"} class="btn btn-outline btn-sm">
<.link
navigate={~p"/info/#{@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 info")}
</.link>
<.link navigate={~p"/packets/#{@callsign}"} class="btn btn-outline btn-sm">
<.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>
<.link navigate={"/?call=#{@callsign}"} class="btn btn-primary btn-sm">
<.link
navigate={"/?call=#{@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("View on map")}
</.link>
</div>
@ -54,15 +63,20 @@
</div>
</div>
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<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-6">
<div class="px-6 py-6">
<% dt =
AprsmeWeb.TimeHelpers.to_datetime(
Map.get(@weather_packet, :inserted_at) || Map.get(@weather_packet, "inserted_at")
) %>
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg class="h-4 w-4 text-primary" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<svg
class="h-4 w-4 text-indigo-600 dark:text-indigo-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.75.75 0 00.736-.686L11.477 4.5a.75.75 0 00-1.491-.154L9.477 9.5H9z"
@ -76,7 +90,7 @@
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Last WX report")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Last WX report")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if dt do %>
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(dt)})<br />
@ -88,7 +102,7 @@
</div>
<%= if has_weather_field?(@weather_packet, :temperature) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Temperature")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Temperature")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :temperature, @locale)}
</dd>
@ -96,7 +110,7 @@
<% end %>
<%= if has_weather_field?(@weather_packet, :humidity) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Humidity")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Humidity")}</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
</dd>
@ -104,7 +118,7 @@
<% end %>
<%= if has_weather_field?(@weather_packet, :wind_direction) || has_weather_field?(@weather_packet, :wind_speed) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Wind")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Wind")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if has_weather_field?(@weather_packet, :wind_direction) do %>
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}°
@ -117,7 +131,7 @@
<% end %>
<%= if has_weather_field?(@weather_packet, :wind_gust) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Gusts")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Gusts")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :wind_gust, @locale)}
</dd>
@ -125,7 +139,7 @@
<% end %>
<%= if has_weather_field?(@weather_packet, :pressure) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Pressure")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Pressure")}</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
</dd>
@ -133,7 +147,7 @@
<% end %>
<%= if has_weather_field?(@weather_packet, :rain_1h) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Rain (1h)")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Rain (1h)")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :rain_1h, @locale)}
</dd>
@ -141,7 +155,7 @@
<% end %>
<%= if has_weather_field?(@weather_packet, :rain_24h) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Rain (24h)")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Rain (24h)")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :rain_24h, @locale)}
</dd>
@ -149,14 +163,14 @@
<% end %>
<%= if has_weather_field?(@weather_packet, :rain_since_midnight) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Rain (since midnight)")}</dt>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Rain (since midnight)")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :rain_since_midnight, @locale)}
</dd>
</div>
<% end %>
<div class="col-span-2">
<dt class="text-xs font-medium opacity-70">{gettext("Raw Packet")}</dt>
<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(@weather_packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(@weather_packet.raw_packet)}
@ -166,14 +180,14 @@
</dd>
</div>
</dl>
<div class="mt-2 text-xs opacity-70">
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<strong>{gettext("Raw comment")}:</strong> {@weather_packet.comment ||
@weather_packet["comment"]}
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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">
<h3 class="text-sm font-medium mb-2">{gettext("Weather History Graphs")}</h3>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :temperature, nil)))) do %>
<div
@ -181,7 +195,7 @@
phx-hook="ChartJSTempChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -193,7 +207,7 @@
phx-hook="ChartJSHumidityChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -205,7 +219,7 @@
phx-hook="ChartJSPressureChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -217,7 +231,7 @@
phx-hook="ChartJSWindChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -229,7 +243,7 @@
phx-hook="ChartJSRainChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -241,7 +255,7 @@
phx-hook="ChartJSLuminosityChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -253,11 +267,11 @@
</div>
</div>
<% else %>
<div class="bg-base-200 flex items-center justify-center min-h-screen">
<div class="card bg-base-100 shadow-xl max-w-lg">
<div class="card-body text-center">
<div class="bg-gray-50 dark:bg-gray-900 flex items-center justify-center min-h-screen">
<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 max-w-lg">
<div class="px-6 py-8 text-center">
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
<div class="opacity-70">No recent weather data available for this callsign.</div>
<div class="text-gray-500 dark:text-gray-400">No recent weather data available for this callsign.</div>
</div>
</div>
</div>

View file

@ -7,15 +7,14 @@ defmodule AprsmeWeb.BadPacketsLiveTest do
alias Aprsme.Repo
describe "Index" do
test "renders bad packets page with DaisyUI card", %{conn: conn} do
test "renders bad packets page with card", %{conn: conn} do
{:ok, _index_live, html} = live(conn, ~p"/badpackets", on_error: :warn)
assert html =~ "card"
assert html =~ "bg-base-100"
assert html =~ "shadow-xl"
assert html =~ "shadow-sm"
assert html =~ "sm:rounded-lg"
end
test "lists all bad packets with DaisyUI table", %{conn: conn} do
test "lists all bad packets with table", %{conn: conn} do
bad_packet =
Repo.insert!(%BadPacket{
raw_packet: "KD9PDP>APRS:Invalid packet data",
@ -29,15 +28,15 @@ defmodule AprsmeWeb.BadPacketsLiveTest do
assert html =~ bad_packet.error_message
assert html =~ bad_packet.error_type
assert html =~ "table"
assert html =~ "badge"
assert html =~ "rounded-md"
end
test "displays empty state with DaisyUI components when no bad packets", %{conn: conn} do
test "displays empty state when no bad packets", %{conn: conn} do
{:ok, _index_live, html} = live(conn, ~p"/badpackets", on_error: :warn)
assert html =~ "No bad packets"
assert html =~ "All packets are parsing successfully!"
assert html =~ "text-success"
assert html =~ "text-green-600"
end
test "updates in real-time when new bad packet is created", %{conn: conn} do
@ -64,13 +63,11 @@ defmodule AprsmeWeb.BadPacketsLiveTest do
assert html =~ bad_packet.error_type
end
test "works correctly in dark mode", %{conn: conn} do
test "works correctly with dark mode support", %{conn: conn} do
{:ok, _index_live, html} = live(conn, ~p"/badpackets", on_error: :warn)
# DaisyUI uses data-theme for theming
# The components should work with both light and dark themes
assert html =~ "bg-base-100"
assert html =~ "text-base-content"
assert html =~ "dark:bg-gray-800"
assert html =~ "dark:text-gray-400"
end
end
end