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:
parent
f9af61f29a
commit
4de48294c6
26 changed files with 1640 additions and 2540 deletions
|
|
@ -12,86 +12,8 @@
|
||||||
The heroicons installation itself is managed by your mix.exs */
|
The heroicons installation itself is managed by your mix.exs */
|
||||||
@plugin "../vendor/heroicons";
|
@plugin "../vendor/heroicons";
|
||||||
|
|
||||||
/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
|
/* Dark mode uses data-theme="dark" attribute on <html>, set via localStorage */
|
||||||
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
|
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add variants based on LiveView classes */
|
/* Add variants based on LiveView classes */
|
||||||
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
|
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
|
||||||
|
|
@ -105,27 +27,6 @@
|
||||||
|
|
||||||
/* This file is for your main application CSS */
|
/* 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 styles */
|
||||||
.marker-cluster-small {
|
.marker-cluster-small {
|
||||||
background-color: rgba(16, 185, 129, 0.8);
|
background-color: rgba(16, 185, 129, 0.8);
|
||||||
|
|
@ -333,59 +234,3 @@
|
||||||
.historical-dot-marker {
|
.historical-dot-marker {
|
||||||
z-index: 15 !important;
|
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;
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,7 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimize header for mobile */
|
/* Optimize header for mobile */
|
||||||
.navbar {
|
.site-header {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -84,10 +84,8 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile-friendly buttons */
|
/* Mobile-friendly buttons */
|
||||||
.btn, button {
|
button {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
padding: 12px 16px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide non-essential elements on small screens */
|
/* Hide non-essential elements on small screens */
|
||||||
|
|
@ -152,7 +150,7 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Safe area insets for devices with notches */
|
/* Safe area insets for devices with notches */
|
||||||
.navbar {
|
.site-header {
|
||||||
padding-left: env(safe-area-inset-left);
|
padding-left: env(safe-area-inset-left);
|
||||||
padding-right: env(safe-area-inset-right);
|
padding-right: env(safe-area-inset-right);
|
||||||
padding-top: env(safe-area-inset-top);
|
padding-top: env(safe-area-inset-top);
|
||||||
|
|
@ -165,7 +163,7 @@ html, body {
|
||||||
|
|
||||||
/* Landscape mode optimizations */
|
/* Landscape mode optimizations */
|
||||||
@media (max-width: 768px) and (orientation: landscape) {
|
@media (max-width: 768px) and (orientation: landscape) {
|
||||||
.navbar {
|
.site-header {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
124
assets/vendor/daisyui-theme.js
vendored
124
assets/vendor/daisyui-theme.js
vendored
File diff suppressed because one or more lines are too long
1031
assets/vendor/daisyui.js
vendored
1031
assets/vendor/daisyui.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -224,7 +224,7 @@ defmodule AprsmeWeb.CoreComponents do
|
||||||
def simple_form(assigns) do
|
def simple_form(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
<.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)}
|
{render_slot(@inner_block, f)}
|
||||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||||
{render_slot(action, f)}
|
{render_slot(action, f)}
|
||||||
|
|
@ -433,32 +433,47 @@ defmodule AprsmeWeb.CoreComponents do
|
||||||
|
|
||||||
def header(assigns) do
|
def header(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="navbar bg-base-200 shadow-lg">
|
<header class="site-header bg-white shadow-sm dark:bg-gray-800/50 dark:shadow-none dark:border-b dark:border-white/10">
|
||||||
<div class="navbar-start">
|
<nav class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
||||||
<.link navigate="/" class="btn btn-ghost text-xl font-bold text-primary">
|
<div class="flex items-center">
|
||||||
aprs.me
|
<.link
|
||||||
<%= if @dev_mode do %>
|
navigate="/"
|
||||||
<span class="badge badge-warning badge-sm ml-2">DEV</span>
|
class="text-xl font-bold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
<% end %>
|
>
|
||||||
</.link>
|
aprs.me
|
||||||
</div>
|
<%= if @dev_mode do %>
|
||||||
<div class="navbar-center hidden lg:flex">
|
<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">
|
||||||
<.navigation variant={:horizontal} current_user={@current_user} />
|
DEV
|
||||||
</div>
|
</span>
|
||||||
<div class="navbar-end">
|
<% end %>
|
||||||
<.theme_selector />
|
</.link>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="hidden lg:flex lg:items-center lg:gap-x-8">
|
||||||
</div>
|
<.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
|
end
|
||||||
|
|
||||||
|
|
@ -467,46 +482,58 @@ defmodule AprsmeWeb.CoreComponents do
|
||||||
"""
|
"""
|
||||||
def theme_selector(assigns) do
|
def theme_selector(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="dropdown dropdown-end">
|
<div class="relative">
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
|
<button
|
||||||
<svg class="w-5 h-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
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" />
|
<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>
|
</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>
|
</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>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
@ -746,74 +773,98 @@ defmodule AprsmeWeb.CoreComponents do
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<%= if @variant == :horizontal do %>
|
<%= if @variant == :horizontal do %>
|
||||||
<ul class="menu menu-horizontal px-1">
|
<.link
|
||||||
<li>
|
navigate={@home_url}
|
||||||
<.link navigate={@home_url} class="text-base-content hover:text-base-content/70">
|
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
|
||||||
{gettext("Home")}
|
>
|
||||||
</.link>
|
{gettext("Home")}
|
||||||
</li>
|
</.link>
|
||||||
<li><.link navigate="/api" class="text-base-content hover:text-base-content/70">API</.link></li>
|
<.link
|
||||||
<li>
|
navigate="/api"
|
||||||
<.link navigate="/about" class="text-base-content hover:text-base-content/70">
|
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
|
||||||
{gettext("About")}
|
>
|
||||||
</.link>
|
API
|
||||||
</li>
|
</.link>
|
||||||
<%= if @current_user do %>
|
<.link
|
||||||
<li>
|
navigate="/about"
|
||||||
<.link navigate="/users/settings" class="text-base-content hover:text-base-content/70">
|
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
|
||||||
{gettext("Settings")}
|
>
|
||||||
</.link>
|
{gettext("About")}
|
||||||
</li>
|
</.link>
|
||||||
<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>
|
|
||||||
<%= if @current_user do %>
|
<%= if @current_user do %>
|
||||||
<li>
|
<.link
|
||||||
<.link navigate="/users/settings" class="text-base-content hover:text-base-content/70">
|
navigate="/users/settings"
|
||||||
{gettext("Settings")}
|
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
|
||||||
</.link>
|
>
|
||||||
</li>
|
{gettext("Settings")}
|
||||||
<li>
|
</.link>
|
||||||
<.link href="/users/log_out" method="delete" class="text-base-content hover:text-base-content/70">
|
<.link
|
||||||
{gettext("Log out")}
|
href="/users/log_out"
|
||||||
</.link>
|
method="delete"
|
||||||
</li>
|
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 %>
|
<% else %>
|
||||||
<li>
|
<.link
|
||||||
<.link navigate="/users/register" class="text-base-content hover:text-base-content/70">
|
navigate="/users/register"
|
||||||
{gettext("Register")}
|
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
|
||||||
</.link>
|
>
|
||||||
</li>
|
{gettext("Register")}
|
||||||
<li>
|
</.link>
|
||||||
<.link navigate="/users/log_in" class="text-base-content hover:text-base-content/70">
|
<.link
|
||||||
{gettext("Log in")}
|
navigate="/users/log_in"
|
||||||
</.link>
|
class="text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-white"
|
||||||
</li>
|
>
|
||||||
|
{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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,20 @@ defmodule AprsmeWeb.Components.InfoMapComponent do
|
||||||
data-callsign={@callsign}
|
data-callsign={@callsign}
|
||||||
data-symbol-html={@symbol_html}
|
data-symbol-html={@symbol_html}
|
||||||
>
|
>
|
||||||
<div id={"#{@id}-loading"} class="h-full w-full bg-base-200 flex items-center justify-center">
|
<div id={"#{@id}-loading"} class="h-full w-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@ defmodule AprsmeWeb.Layouts do
|
||||||
embed_templates "layouts/*"
|
embed_templates "layouts/*"
|
||||||
|
|
||||||
def body_class(_assigns) do
|
def body_class(_assigns) do
|
||||||
["bg-base-100 antialiased"]
|
["bg-white dark:bg-gray-900 text-gray-900 dark:text-white antialiased"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Map page specific styles - hide navbar header */
|
/* Map page specific styles - hide site header */
|
||||||
body:has(#main-content[data-map-page="true"]) .navbar {
|
body:has(#main-content[data-map-page="true"]) .site-header {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 352px;
|
width: 352px;
|
||||||
background: var(--color-base-100);
|
background: white;
|
||||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
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} />
|
<.header dev_mode={Application.get_env(:aprsme, :dev_routes, false)} current_user={@current_user} />
|
||||||
<main
|
<main
|
||||||
id="main-content"
|
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)}
|
data-map-page={to_string(assigns[:map_page] || false)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,45 @@
|
||||||
<div class="container mx-auto max-w-2xl py-12">
|
<div class="mx-auto max-w-2xl py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="card bg-base-100 shadow-xl mb-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="card-body prose max-w-none">
|
<div class="px-6 py-8 prose max-w-none dark:prose-invert">
|
||||||
<h2 class="card-title text-3xl">{gettext("Under heavy construction")}</h2>
|
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">{gettext("Under heavy construction")}</h2>
|
||||||
<p>
|
<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.
|
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>
|
<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.
|
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>
|
</p>
|
||||||
<div class="bg-base-200 p-6 rounded-lg mb-8">
|
<div class="bg-gray-50 p-6 rounded-lg mb-8 dark:bg-gray-700/50">
|
||||||
<h4 class="text-lg font-semibold mb-3">{gettext("Built with Modern Technologies")}</h4>
|
<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">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span class="badge badge-primary">Elixir</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">
|
||||||
<span class="badge badge-success">Phoenix</span>
|
Elixir
|
||||||
<span class="badge badge-secondary">LiveView</span>
|
</span>
|
||||||
<span class="badge badge-warning">PostgreSQL</span>
|
<span class="inline-flex items-center rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 dark:bg-green-400/10 dark:text-green-400">
|
||||||
<span class="badge badge-error">Tailwind CSS</span>
|
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>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-2xl font-semibold mb-4">{gettext("Join Our Community")}</h3>
|
<h3 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">{gettext("Join Our Community")}</h3>
|
||||||
<p>
|
<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.
|
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>
|
</p>
|
||||||
<div class="text-center mt-12">
|
<div class="text-center mt-12">
|
||||||
<a href="/" class="btn btn-primary btn-lg">
|
<a
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
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
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|
@ -37,7 +52,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<p class="text-sm">
|
||||||
© 2025 aprs.me. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
© 2025 aprs.me. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -217,49 +217,57 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
<div class="container mx-auto max-w-6xl py-8">
|
<div class="container mx-auto max-w-6xl py-8">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-4xl font-bold mb-4">APRS.me API Documentation</h1>
|
<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.
|
RESTful JSON API for accessing APRS packet data and station information.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Overview -->
|
<!-- API Overview -->
|
||||||
<div class="card bg-base-100 shadow-xl mb-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="card-body">
|
<div class="px-6 py-8">
|
||||||
<h2 class="card-title text-2xl">Overview</h2>
|
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white">Overview</h2>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
The APRS.me API provides programmatic access to Amateur Radio APRS (Automatic Packet Reporting System)
|
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.
|
data. All API endpoints return JSON data and follow RESTful conventions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-6 mt-6">
|
<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>
|
<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
|
https://aprs.me/api/v1
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</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>
|
<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
|
application/json
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning mt-6">
|
<div class="rounded-md bg-yellow-50 p-4 dark:bg-yellow-500/10 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">
|
<div class="flex">
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
class="shrink-0 h-6 w-6 text-yellow-700 dark:text-yellow-400"
|
||||||
stroke-width="2"
|
fill="none"
|
||||||
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"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke="currentColor"
|
||||||
</svg>
|
>
|
||||||
<div>
|
<path
|
||||||
<h3 class="font-bold">Rate Limiting</h3>
|
stroke-linecap="round"
|
||||||
<div class="text-xs">
|
stroke-linejoin="round"
|
||||||
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests. Rate limiting may be implemented in the future.
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -269,16 +277,18 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
<!-- API Endpoints -->
|
<!-- API Endpoints -->
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<!-- Callsign Endpoint -->
|
<!-- Callsign Endpoint -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-8">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="card-title text-xl">Get Latest Packet by Callsign</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Get Latest Packet by Callsign</h2>
|
||||||
<span class="badge badge-success">GET</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">
|
||||||
|
GET
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="text-lg font-medium mb-2">Endpoint</h3>
|
<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>
|
<code class="text-sm">GET /api/v1/callsign/{"{callsign}"}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -294,21 +304,25 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="text-lg font-medium mb-2">Parameters</h3>
|
<h3 class="text-lg font-medium mb-2">Parameters</h3>
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead class="text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Parameter</th>
|
<th class="px-3 py-3.5">Parameter</th>
|
||||||
<th>Type</th>
|
<th class="px-3 py-3.5">Type</th>
|
||||||
<th>Required</th>
|
<th class="px-3 py-3.5">Required</th>
|
||||||
<th>Description</th>
|
<th class="px-3 py-3.5">Description</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">callsign</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
|
||||||
<td>string</td>
|
callsign
|
||||||
<td>Yes</td>
|
</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">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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -317,7 +331,7 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="text-lg font-medium mb-2">Example Request</h3>
|
<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" \
|
<pre><code>curl -X GET "https://aprs.me/api/v1/callsign/N0CALL-9" \
|
||||||
-H "Accept: application/json"</code></pre>
|
-H "Accept: application/json"</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -327,7 +341,7 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
<h3 class="text-lg font-medium mb-2">Response Format</h3>
|
<h3 class="text-lg font-medium mb-2">Response Format</h3>
|
||||||
|
|
||||||
<h4 class="text-md font-medium mb-2">Success Response (200 OK)</h4>
|
<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|{
|
<pre><code><%= raw ~s|{
|
||||||
"data": {
|
"data": {
|
||||||
"id": 12345,
|
"id": 12345,
|
||||||
|
|
@ -370,7 +384,7 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="text-md font-medium mb-2">Not Found Response (404)</h4>
|
<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|{
|
<pre><code><%= raw ~s|{
|
||||||
"data": null,
|
"data": null,
|
||||||
"message": "No packets found for callsign N0CALL-9"
|
"message": "No packets found for callsign N0CALL-9"
|
||||||
|
|
@ -378,7 +392,7 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="text-md font-medium mb-2">Error Response (400)</h4>
|
<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|{
|
<pre><code><%= raw ~s|{
|
||||||
"error": {
|
"error": {
|
||||||
"message": "Invalid callsign format",
|
"message": "Invalid callsign format",
|
||||||
|
|
@ -391,73 +405,97 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Response Fields Documentation -->
|
<!-- Response Fields Documentation -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-8">
|
||||||
<h2 class="card-title text-xl">Response Fields</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Response Fields</h2>
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead class="text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Field</th>
|
<th class="px-3 py-3.5">Field</th>
|
||||||
<th>Type</th>
|
<th class="px-3 py-3.5">Type</th>
|
||||||
<th>Description</th>
|
<th class="px-3 py-3.5">Description</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">id</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">id</td>
|
||||||
<td>integer</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">integer</td>
|
||||||
<td>Unique packet identifier</td>
|
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Unique packet identifier</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">callsign</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">callsign</td>
|
||||||
<td>string</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||||
<td>Full callsign with SSID (e.g., "N0CALL-9")</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">base_callsign</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
|
||||||
<td>string</td>
|
base_callsign
|
||||||
<td>Base callsign without SSID</td>
|
</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">ssid</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">ssid</td>
|
||||||
<td>string</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string</td>
|
||||||
<td>SSID (Secondary Station Identifier), null if not present</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">received_at</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
|
||||||
<td>datetime</td>
|
received_at
|
||||||
<td>When the packet was received (ISO 8601 format)</td>
|
</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">position</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">position</td>
|
||||||
<td>object</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
|
||||||
<td>Position data (latitude, longitude, course, speed, altitude)</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">symbol</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">symbol</td>
|
||||||
<td>object</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
|
||||||
<td>APRS symbol information (code and table_id)</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">weather</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">weather</td>
|
||||||
<td>object</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
|
||||||
<td>Weather data if present (temperature, humidity, wind, etc.)</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">equipment</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
|
||||||
<td>object</td>
|
equipment
|
||||||
<td>Equipment information (manufacturer, type)</td>
|
</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">message</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">message</td>
|
||||||
<td>object</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
|
||||||
<td>Message data if the packet is a message</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono">raw_packet</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
|
||||||
<td>string</td>
|
raw_packet
|
||||||
<td>Original raw APRS packet as received</td>
|
</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -466,37 +504,53 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTTP Status Codes -->
|
<!-- HTTP Status Codes -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-8">
|
||||||
<h2 class="card-title text-xl">HTTP Status Codes</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">HTTP Status Codes</h2>
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead class="text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status Code</th>
|
<th class="px-3 py-3.5">Status Code</th>
|
||||||
<th>Description</th>
|
<th class="px-3 py-3.5">Description</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono text-success">200 OK</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-green-600 dark:text-green-400">200 OK</td>
|
||||||
<td>Request successful, packet data returned</td>
|
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Request successful, packet data returned
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono text-warning">400 Bad Request</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-yellow-600 dark:text-yellow-400">
|
||||||
<td>Invalid callsign format or malformed request</td>
|
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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono text-error">404 Not Found</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-red-600 dark:text-red-400">
|
||||||
<td>No packets found for the specified callsign</td>
|
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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono text-error">408 Request Timeout</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-red-600 dark:text-red-400">
|
||||||
<td>Request took too long to process</td>
|
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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-mono text-error">500 Internal Server Error</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-red-600 dark:text-red-400">
|
||||||
<td>Server error occurred while processing the request</td>
|
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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -505,53 +559,61 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Future Endpoints -->
|
<!-- Future Endpoints -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-8">
|
||||||
<h2 class="card-title text-xl">Planned Endpoints</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Planned Endpoints</h2>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
The following endpoints are planned for future releases:
|
The following endpoints are planned for future releases:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<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>
|
<div>
|
||||||
<code class="text-sm font-mono">GET /api/v1/callsign/{"{callsign}"}/history</code>
|
<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>
|
</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 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>
|
<div>
|
||||||
<code class="text-sm font-mono">GET /api/v1/packets/recent</code>
|
<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>
|
</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 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>
|
<div>
|
||||||
<code class="text-sm font-mono">GET /api/v1/packets/area</code>
|
<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>
|
</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 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>
|
<div>
|
||||||
<code class="text-sm font-mono">GET /api/v1/weather/{"{callsign}"}</code>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Interactive API Testing -->
|
<!-- Interactive API Testing -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-8">
|
||||||
<h2 class="card-title text-xl">Test the API</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Test the API</h2>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
Try the API directly from this page. Enter a callsign to see the most recent packet data.
|
Try the API directly from this page. Enter a callsign to see the most recent packet data.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -559,10 +621,10 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
<div class="max-w-md">
|
<div class="max-w-md">
|
||||||
<form phx-submit="test_api" class="space-y-4">
|
<form phx-submit="test_api" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="test_callsign" class="label">
|
<label for="test_callsign" class="block text-sm/6 font-medium text-gray-900 dark:text-white">
|
||||||
<span class="label-text">Callsign (e.g., N0CALL or N0CALL-9)</span>
|
Callsign (e.g., N0CALL or N0CALL-9)
|
||||||
</label>
|
</label>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2 mt-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="test_callsign"
|
id="test_callsign"
|
||||||
|
|
@ -570,12 +632,29 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
value={@test_callsign}
|
value={@test_callsign}
|
||||||
phx-change="update_callsign"
|
phx-change="update_callsign"
|
||||||
placeholder="Enter 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}
|
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 %>
|
<%= 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 %>
|
<% else %>
|
||||||
Test API
|
Test API
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -586,21 +665,24 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
|
|
||||||
<!-- Error Display -->
|
<!-- Error Display -->
|
||||||
<%= if @error do %>
|
<%= if @error do %>
|
||||||
<div class="alert alert-error mt-4">
|
<div class="rounded-md bg-red-50 p-4 dark:bg-red-500/10 mt-4">
|
||||||
<svg
|
<div class="flex">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
class="shrink-0 h-6 w-6 text-red-700 dark:text-red-400"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
>
|
||||||
stroke-linejoin="round"
|
<path
|
||||||
stroke-width="2"
|
stroke-linecap="round"
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
stroke-linejoin="round"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
<span>{@error}</span>
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="ml-3 text-sm text-red-700 dark:text-red-400">{@error}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
@ -609,9 +691,9 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<h4 class="text-lg font-medium mb-2">API Response</h4>
|
<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">
|
<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
|
<path
|
||||||
fill-rule="evenodd"
|
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"
|
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>
|
<span class="font-medium">JSON Response</span>
|
||||||
</div>
|
</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>
|
<pre><%= @api_result %></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -632,15 +714,15 @@ defmodule AprsmeWeb.ApiDocsLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact and Support -->
|
<!-- Contact and Support -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-8">
|
||||||
<h2 class="card-title text-xl">Support</h2>
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Support</h2>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
This API is provided free of charge for amateur radio and educational purposes.
|
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.
|
If you encounter issues or have suggestions for improvements, please reach out.
|
||||||
</p>
|
</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>
|
<h3 class="font-semibold mb-2">Guidelines</h3>
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li>• Use reasonable request rates to avoid overwhelming the service</li>
|
<li>• Use reasonable request rates to avoid overwhelming the service</li>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<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")}
|
{gettext("Auto-refreshes every 5 seconds")}
|
||||||
</span>
|
</span>
|
||||||
<%= if @loading do %>
|
<%= if @loading do %>
|
||||||
<span class="flex items-center gap-2 text-sm text-primary">
|
<span class="flex items-center gap-2 text-sm text-indigo-600 dark:text-indigo-400">
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<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...")}
|
{gettext("Updating...")}
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{gettext("Attempted At")}</th>
|
<th>{gettext("Attempted At")}</th>
|
||||||
|
|
@ -35,18 +47,18 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<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")}
|
{bad_packet.error_type || gettext("Unknown")}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<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")}
|
{bad_packet.error_message || gettext("No error message")}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="max-w-md">
|
<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 %>
|
<%= if is_binary(bad_packet.raw_packet) do %>
|
||||||
{Aprsme.EncodingUtils.sanitize_string(bad_packet.raw_packet)}
|
{Aprsme.EncodingUtils.sanitize_string(bad_packet.raw_packet)}
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
@ -64,11 +76,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if length(@bad_packets) == 0 do %>
|
<%= if length(@bad_packets) == 0 do %>
|
||||||
<div class="card bg-base-100 shadow-xl mt-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 mt-8">
|
||||||
<div class="card-body text-center">
|
<div class="px-6 py-8 text-center">
|
||||||
<div class="flex justify-center mb-4">
|
<div class="flex justify-center mb-4">
|
||||||
<div class="w-16 h-16 bg-success/20 rounded-full flex items-center justify-center">
|
<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-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
|
class="w-8 h-8 text-green-600 dark:text-green-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|
@ -79,7 +96,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold">{gettext("No bad packets")}</h3>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<!-- Leaflet is already loaded via vendor bundles in the layout -->
|
<!-- 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 -->
|
<!-- 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="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="py-4 md:flex md:items-center md:justify-between">
|
<div class="py-4 md:flex md:items-center md:justify-between">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
{gettext("APRS Station")}
|
{gettext("APRS Station")}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="ml-3 flex items-center space-x-3">
|
<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}
|
{@callsign}
|
||||||
</span>
|
</span>
|
||||||
<%= if @packet do %>
|
<%= if @packet do %>
|
||||||
|
|
@ -30,11 +30,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex md:ml-4 md:mt-0">
|
<div class="mt-3 flex md:ml-4 md:mt-0">
|
||||||
<div class="flex space-x-2">
|
<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")}
|
{gettext("View packets")}
|
||||||
</.link>
|
</.link>
|
||||||
<%= if @has_weather_packets do %>
|
<%= 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")}
|
{gettext("Weather charts")}
|
||||||
</.link>
|
</.link>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -49,11 +55,17 @@
|
||||||
<!-- Station details -->
|
<!-- Station details -->
|
||||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mb-6 lg:grid-rows-1">
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mb-6 lg:grid-rows-1">
|
||||||
<!-- Position information -->
|
<!-- Position information -->
|
||||||
<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">
|
||||||
<div class="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="flex-shrink-0">
|
<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" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
|
|
@ -68,14 +80,14 @@
|
||||||
</div>
|
</div>
|
||||||
<dl class="grid grid-cols-2 gap-3">
|
<dl class="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{@packet.lat || ""}, {@packet.lon || ""}
|
{@packet.lat || ""}, {@packet.lon || ""}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<%= if @packet.lat && @packet.lon do %>
|
<%= if @packet.lat && @packet.lon do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
<%= case Gridsquare.encode(@packet.lon, @packet.lat) do %>
|
<%= case Gridsquare.encode(@packet.lon, @packet.lat) do %>
|
||||||
<% %Gridsquare.EncodeResult{grid_reference: grid_ref} -> %>
|
<% %Gridsquare.EncodeResult{grid_reference: grid_ref} -> %>
|
||||||
|
|
@ -87,7 +99,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm">
|
||||||
<%= if @packet.received_at do %>
|
<%= if @packet.received_at do %>
|
||||||
<div
|
<div
|
||||||
|
|
@ -98,35 +110,35 @@
|
||||||
>
|
>
|
||||||
{AprsmeWeb.TimeHelpers.time_ago_in_words(@packet.received_at)}
|
{AprsmeWeb.TimeHelpers.time_ago_in_words(@packet.received_at)}
|
||||||
</div>
|
</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")}
|
{Calendar.strftime(@packet.received_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="opacity-70">{gettext("Unknown")}</span>
|
<span class="text-gray-500 dark:text-gray-400">{gettext("Unknown")}</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<%= if @packet.altitude do %>
|
<%= if @packet.altitude do %>
|
||||||
<div>
|
<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>
|
<dd class="mt-1 text-sm font-semibold">{@packet.altitude} ft</dd>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @packet.course do %>
|
<%= if @packet.course do %>
|
||||||
<div>
|
<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>
|
<dd class="mt-1 text-sm font-semibold">{@packet.course}°</dd>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @packet.speed do %>
|
<%= if @packet.speed do %>
|
||||||
<div>
|
<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>
|
<dd class="mt-1 text-sm font-semibold">{@packet.speed} MPH</dd>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @packet.comment do %>
|
<%= if @packet.comment do %>
|
||||||
<div class="col-span-2">
|
<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">
|
<dd class="mt-1 text-sm">
|
||||||
<%= if is_binary(@packet.comment) do %>
|
<%= if is_binary(@packet.comment) do %>
|
||||||
{Aprsme.EncodingUtils.sanitize_string(@packet.comment)}
|
{Aprsme.EncodingUtils.sanitize_string(@packet.comment)}
|
||||||
|
|
@ -138,31 +150,31 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @packet.phg_power || @packet.phg_height || @packet.phg_gain do %>
|
<%= if @packet.phg_power || @packet.phg_height || @packet.phg_gain do %>
|
||||||
<div class="col-span-2">
|
<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)")}
|
{gettext("PHG (Power-Height-Gain)")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="grid grid-cols-2 gap-2 text-sm">
|
<dd class="grid grid-cols-2 gap-2 text-sm">
|
||||||
<%= if @packet.phg_power do %>
|
<%= if @packet.phg_power do %>
|
||||||
<div>
|
<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>
|
<span class="font-semibold">{@packet.phg_power} W</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @packet.phg_height do %>
|
<%= if @packet.phg_height do %>
|
||||||
<div>
|
<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>
|
<span class="font-semibold">{@packet.phg_height} ft</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @packet.phg_gain do %>
|
<%= if @packet.phg_gain do %>
|
||||||
<div>
|
<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>
|
<span class="font-semibold">{@packet.phg_gain} dBi</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if @packet.phg_directivity do %>
|
<%= if @packet.phg_directivity do %>
|
||||||
<div>
|
<div>
|
||||||
<span class="opacity-70">{gettext("Dir:")}</span>
|
<span class="text-gray-500 dark:text-gray-400">{gettext("Dir:")}</span>
|
||||||
<span class="font-semibold">
|
<span class="font-semibold">
|
||||||
<%= if @packet.phg_directivity == 0 or @packet.phg_directivity == 360 do %>
|
<%= if @packet.phg_directivity == 0 or @packet.phg_directivity == 360 do %>
|
||||||
{gettext("Omni")}
|
{gettext("Omni")}
|
||||||
|
|
@ -181,7 +193,7 @@
|
||||||
|
|
||||||
<!-- Map showing station location -->
|
<!-- Map showing station location -->
|
||||||
<%= if @packet && @packet.lat && @packet.lon do %>
|
<%= 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_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(@packet) %>
|
||||||
<% symbol_html = AprsmeWeb.AprsSymbol.render_marker_html(symbol_table, symbol_code, @callsign, 32) %>
|
<% symbol_html = AprsmeWeb.AprsSymbol.render_marker_html(symbol_table, symbol_code, @callsign, 32) %>
|
||||||
<.info_map
|
<.info_map
|
||||||
|
|
@ -199,12 +211,12 @@
|
||||||
<!-- Row for Device Information and Other SSIDs -->
|
<!-- Row for Device Information and Other SSIDs -->
|
||||||
<div class="lg:col-span-2 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div class="lg:col-span-2 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<!-- Device information -->
|
<!-- Device information -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg
|
<svg
|
||||||
class="h-4 w-4 opacity-70"
|
class="h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
|
|
@ -223,7 +235,7 @@
|
||||||
</div>
|
</div>
|
||||||
<dl class="grid grid-cols-2 gap-3">
|
<dl class="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
<%= if @packet.device_model || @packet.device_vendor do %>
|
<%= if @packet.device_model || @packet.device_vendor do %>
|
||||||
{[
|
{[
|
||||||
|
|
@ -237,41 +249,44 @@
|
||||||
({@packet.device_class})
|
({@packet.device_class})
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="opacity-70 font-mono">
|
<span class="text-gray-500 dark:text-gray-400 font-mono">
|
||||||
{@packet.device_identifier || gettext("Unknown")}
|
{@packet.device_identifier || gettext("Unknown")}
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
<% path_elements = parse_path_with_links(@packet.path) %>
|
<% path_elements = parse_path_with_links(@packet.path) %>
|
||||||
<%= for {{type, content, display}, index} <- Enum.with_index(path_elements) do %>
|
<%= for {{type, content, display}, index} <- Enum.with_index(path_elements) do %>
|
||||||
<%= case type do %>
|
<%= case type do %>
|
||||||
<% :link -> %>
|
<% :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}
|
{display}
|
||||||
</.link>
|
</.link>
|
||||||
<% :text -> %>
|
<% :text -> %>
|
||||||
{content}
|
{content}
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if index < length(path_elements) - 1 do %>
|
<%= if index < length(path_elements) - 1 do %>
|
||||||
<span class="opacity-50">,</span>
|
<span class="text-gray-400 dark:text-gray-500">,</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<%= if @packet.path && @packet.path != "" do %>
|
<%= if @packet.path && @packet.path != "" do %>
|
||||||
<div class="col-span-2">
|
<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">
|
<dd class="mt-1 text-sm">
|
||||||
{decode_aprs_path(@packet.path)}
|
{decode_aprs_path(@packet.path)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="col-span-2">
|
<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">
|
<dd class="mt-1 text-xs font-mono break-all">
|
||||||
<%= if is_binary(@packet.raw_packet) do %>
|
<%= if is_binary(@packet.raw_packet) do %>
|
||||||
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
|
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
|
||||||
|
|
@ -286,12 +301,12 @@
|
||||||
|
|
||||||
<!-- Other SSIDs -->
|
<!-- Other SSIDs -->
|
||||||
<%= if length(@other_ssids) > 0 do %>
|
<%= if length(@other_ssids) > 0 do %>
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<svg
|
<svg
|
||||||
class="h-4 w-4 opacity-70"
|
class="h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
|
|
@ -309,7 +324,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-xs font-medium uppercase tracking-wider">
|
<th class="text-xs font-medium uppercase tracking-wider">
|
||||||
|
|
@ -328,7 +343,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-medium">
|
<td class="font-medium">
|
||||||
<div class="flex items-center space-x-2">
|
<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}
|
{ssid_info.callsign}
|
||||||
</.link>
|
</.link>
|
||||||
<%= if ssid_info.packet do %>
|
<%= if ssid_info.packet do %>
|
||||||
|
|
@ -345,7 +363,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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 %>
|
<%= if ssid_info.packet && ssid_info.packet.lat && ssid_info.packet.lon && @packet && @packet.lat && @packet.lon do %>
|
||||||
<% dist =
|
<% dist =
|
||||||
haversine(
|
haversine(
|
||||||
|
|
@ -367,7 +385,7 @@
|
||||||
-
|
-
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if ssid_info.received_at do %>
|
<%= if ssid_info.received_at do %>
|
||||||
<span
|
<span
|
||||||
class="text-sm"
|
class="text-sm"
|
||||||
|
|
@ -378,7 +396,7 @@
|
||||||
{AprsmeWeb.TimeHelpers.time_ago_in_words(ssid_info.received_at)}
|
{AprsmeWeb.TimeHelpers.time_ago_in_words(ssid_info.received_at)}
|
||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% 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 %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -396,11 +414,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Neighboring stations -->
|
<!-- Neighboring stations -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="flex-shrink-0">
|
<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
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|
@ -413,7 +437,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-xs font-medium uppercase tracking-wider">
|
<th class="text-xs font-medium uppercase tracking-wider">
|
||||||
|
|
@ -432,7 +456,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-medium">
|
<td class="font-medium">
|
||||||
<div class="flex items-center space-x-2">
|
<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}
|
{neighbor.callsign}
|
||||||
</.link>
|
</.link>
|
||||||
<%= if neighbor.packet do %>
|
<%= if neighbor.packet do %>
|
||||||
|
|
@ -449,14 +476,14 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if neighbor.course do %>
|
<%= if neighbor.course do %>
|
||||||
{neighbor.distance || ""} @ {Float.round(neighbor.course, 0)}°
|
{neighbor.distance || ""} @ {Float.round(neighbor.course, 0)}°
|
||||||
<% else %>
|
<% else %>
|
||||||
{neighbor.distance || ""}
|
{neighbor.distance || ""}
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if neighbor.last_heard do %>
|
<%= if neighbor.last_heard do %>
|
||||||
<div
|
<div
|
||||||
class="font-semibold"
|
class="font-semibold"
|
||||||
|
|
@ -466,11 +493,11 @@
|
||||||
>
|
>
|
||||||
{neighbor.last_heard.time_ago}
|
{neighbor.last_heard.time_ago}
|
||||||
</div>
|
</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}
|
{neighbor.last_heard.formatted}
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="opacity-70">{gettext("Unknown")}</span>
|
<span class="text-gray-500 dark:text-gray-400">{gettext("Unknown")}</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -483,11 +510,17 @@
|
||||||
|
|
||||||
<!-- Stations that heard this callsign on RF -->
|
<!-- Stations that heard this callsign on RF -->
|
||||||
<%= if length(@heard_by_stations) > 0 do %>
|
<%= if length(@heard_by_stations) > 0 do %>
|
||||||
<div class="card bg-base-100 shadow-xl mt-6">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="flex-shrink-0">
|
<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
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|
@ -504,7 +537,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-xs font-medium uppercase tracking-wider">
|
<th class="text-xs font-medium uppercase tracking-wider">
|
||||||
|
|
@ -528,11 +561,14 @@
|
||||||
<%= for station <- @heard_by_stations do %>
|
<%= for station <- @heard_by_stations do %>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-medium">
|
<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}
|
{station.digipeater}
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if station.first_heard do %>
|
<%= if station.first_heard do %>
|
||||||
<div class="text-xs font-mono">
|
<div class="text-xs font-mono">
|
||||||
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
||||||
|
|
@ -541,12 +577,12 @@
|
||||||
-
|
-
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if station.last_heard do %>
|
<%= if station.last_heard do %>
|
||||||
<div class="font-semibold">
|
<div class="font-semibold">
|
||||||
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
||||||
</div>
|
</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")}
|
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
@ -556,11 +592,11 @@
|
||||||
<td>
|
<td>
|
||||||
{station.packet_count}
|
{station.packet_count}
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if station.longest_distance do %>
|
<%= if station.longest_distance do %>
|
||||||
{station.longest_distance}
|
{station.longest_distance}
|
||||||
<%= if station.longest_path_time do %>
|
<%= 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")}
|
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -579,11 +615,17 @@
|
||||||
|
|
||||||
<!-- Stations heard directly by this callsign -->
|
<!-- Stations heard directly by this callsign -->
|
||||||
<%= if length(@stations_heard_by) > 0 do %>
|
<%= if length(@stations_heard_by) > 0 do %>
|
||||||
<div class="card bg-base-100 shadow-xl mt-6">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="flex-shrink-0">
|
<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
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|
@ -600,7 +642,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-xs font-medium uppercase tracking-wider">
|
<th class="text-xs font-medium uppercase tracking-wider">
|
||||||
|
|
@ -624,11 +666,14 @@
|
||||||
<%= for station <- @stations_heard_by do %>
|
<%= for station <- @stations_heard_by do %>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-medium">
|
<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}
|
{station.station}
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if station.first_heard do %>
|
<%= if station.first_heard do %>
|
||||||
<div class="text-xs font-mono">
|
<div class="text-xs font-mono">
|
||||||
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
||||||
|
|
@ -637,12 +682,12 @@
|
||||||
-
|
-
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if station.last_heard do %>
|
<%= if station.last_heard do %>
|
||||||
<div class="font-semibold">
|
<div class="font-semibold">
|
||||||
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
||||||
</div>
|
</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")}
|
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
@ -652,11 +697,11 @@
|
||||||
<td>
|
<td>
|
||||||
{station.packet_count}
|
{station.packet_count}
|
||||||
</td>
|
</td>
|
||||||
<td class="opacity-70">
|
<td class="text-gray-500 dark:text-gray-400">
|
||||||
<%= if station.longest_distance do %>
|
<%= if station.longest_distance do %>
|
||||||
{station.longest_distance}
|
{station.longest_distance}
|
||||||
<%= if station.longest_path_time do %>
|
<%= 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")}
|
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -675,7 +720,7 @@
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="text-center py-8">
|
<div class="text-center py-8">
|
||||||
<svg
|
<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"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
|
|
@ -689,7 +734,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="mt-2 text-sm font-semibold">{gettext("No data available")}</h3>
|
<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.")}
|
{gettext("No recent packet data available for this callsign.")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1682,14 +1682,14 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
</svg>
|
</svg>
|
||||||
<span class="font-medium">{gettext("Navigation")}</span>
|
<span class="font-medium">{gettext("Navigation")}</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="menu menu-sm text-sm">
|
<div class="text-sm">
|
||||||
<.navigation
|
<.navigation
|
||||||
variant={:vertical}
|
variant={:vertical}
|
||||||
current_user={@current_user}
|
current_user={@current_user}
|
||||||
map_state={%{lat: @map_center.lat, lng: @map_center.lng, zoom: @map_zoom}}
|
map_state={%{lat: @map_center.lat, lng: @map_center.lng, zoom: @map_zoom}}
|
||||||
tracked_callsign={@tracked_callsign}
|
tracked_callsign={@tracked_callsign}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Last Update -->
|
<!-- Last Update -->
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
||||||
<%= if @error do %>
|
<%= 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">
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
|
|
@ -16,10 +16,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
<%= for packet <- @all_packets do %>
|
<%= for packet <- @all_packets do %>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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 %>
|
<%= case packet.received_at do %>
|
||||||
<% %DateTime{} = dt -> %>
|
<% %DateTime{} = dt -> %>
|
||||||
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S")}
|
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S")}
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
<span class="text-sm font-medium">{packet.sender}</span>
|
<span class="text-sm font-medium">{packet.sender}</span>
|
||||||
</td>
|
</td>
|
||||||
<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}
|
{packet.data_type}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -70,12 +70,12 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<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}
|
{packet.path}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<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 %>
|
<%= if packet.device_model || packet.device_vendor do %>
|
||||||
{[packet.device_model, packet.device_vendor]
|
{[packet.device_model, packet.device_vendor]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
<%= if length(@all_packets) == 0 do %>
|
<%= if length(@all_packets) == 0 do %>
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<div class="flex justify-center mb-4">
|
<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
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|
@ -110,14 +110,14 @@
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium mb-2">No packets found for {@callsign}</h3>
|
<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.
|
Packets will appear here as they are received live or if there are stored packets for this callsign.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl mt-4">
|
<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="card-body">
|
<div class="px-6 py-4">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<span>
|
<span>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<div class="container mx-auto max-w-7xl py-8">
|
<div class="container mx-auto max-w-7xl py-8">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<div class="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{gettext("Sender")}</th>
|
<th>{gettext("Sender")}</th>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<td>
|
<td>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
|
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", ""))}
|
{Map.get(packet, :sender, Map.get(packet, "sender", ""))}
|
||||||
</.link>
|
</.link>
|
||||||
|
|
@ -35,13 +35,13 @@
|
||||||
<td>
|
<td>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"}
|
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", ""))}
|
{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<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", ""))}
|
{Map.get(packet, :data_type, Map.get(packet, "data_type", ""))}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<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", ""))}
|
{Map.get(packet, :path, Map.get(packet, "path", ""))}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -148,7 +148,7 @@
|
||||||
<%= if length(@packets) == 0 do %>
|
<%= if length(@packets) == 0 do %>
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<div class="flex justify-center mb-4">
|
<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
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|
@ -158,7 +158,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium mb-2">{gettext("No packets")}</h3>
|
<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>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,104 +8,108 @@ defmodule AprsmeWeb.StatusLive.ConnectionCardComponent do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4">APRS-IS Connection</h2>
|
<h2 class="text-xl font-semibold mb-4">APRS-IS Connection</h2>
|
||||||
<div class="card bg-base-200">
|
<div class="rounded-lg bg-gray-50 p-6 dark:bg-gray-700/50">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="flex items-center">
|
<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 %>
|
<%= if @aprs_status.connected do %>
|
||||||
<div class="badge badge-success gap-1">
|
<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">
|
||||||
<div class="w-2 h-2 bg-current rounded-full"></div>
|
<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
|
Connected
|
||||||
</div>
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="badge badge-error gap-1">
|
<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">
|
||||||
<div class="w-2 h-2 bg-current rounded-full"></div>
|
<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
|
Disconnected
|
||||||
</div>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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">
|
<span class="text-sm font-mono">
|
||||||
{@aprs_status.server}:{@aprs_status.port}
|
{@aprs_status.server}:{@aprs_status.port}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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>
|
<span class="text-sm font-mono">{@aprs_status.login_id}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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">
|
<span class="text-sm">
|
||||||
<%= if @aprs_status.connected_at do %>
|
<%= if @aprs_status.connected_at do %>
|
||||||
{Calendar.strftime(@aprs_status.connected_at, "%Y-%m-%d %H:%M:%S UTC")}
|
{Calendar.strftime(@aprs_status.connected_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="opacity-50">Not connected</span>
|
<span class="text-gray-400 dark:text-gray-500">Not connected</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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={[
|
<span class={[
|
||||||
"text-sm font-medium",
|
"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)}
|
{AprsmeWeb.StatusLive.Index.format_uptime(@aprs_status.uptime_seconds)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center col-span-2">
|
<div class="flex items-center col-span-2">
|
||||||
<span class="text-sm font-medium opacity-70 mr-2">Filter:</span>
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">Filter:</span>
|
||||||
<div class="badge badge-outline font-mono">
|
<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}
|
{@aprs_status.filter}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Packet Statistics -->
|
<!-- Packet Statistics -->
|
||||||
<div class="divider"></div>
|
<div class="border-t border-gray-200 dark:border-white/10 my-4"></div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium mb-3">Packet Statistics</h3>
|
<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="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
<div class="flex items-center">
|
<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">
|
<span class="text-sm font-mono">
|
||||||
{AprsmeWeb.StatusLive.Index.format_number(@aprs_status.packet_stats.total_packets)}
|
{AprsmeWeb.StatusLive.Index.format_number(@aprs_status.packet_stats.total_packets)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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">
|
<span class="text-sm font-mono">
|
||||||
{@aprs_status.packet_stats.packets_per_second}
|
{@aprs_status.packet_stats.packets_per_second}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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">
|
<span class="text-sm font-mono">
|
||||||
{@aprs_status.packet_stats.packets_per_second * 60}
|
{@aprs_status.packet_stats.packets_per_second * 60}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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">
|
<span class="text-sm">
|
||||||
<%= if @aprs_status.packet_stats.last_packet_at do %>
|
<%= if @aprs_status.packet_stats.last_packet_at do %>
|
||||||
{AprsmeWeb.StatusLive.Index.format_time_ago(@aprs_status.packet_stats.last_packet_at)}
|
{AprsmeWeb.StatusLive.Index.format_time_ago(@aprs_status.packet_stats.last_packet_at)}
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="opacity-50">None</span>
|
<span class="text-gray-400 dark:text-gray-500">None</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<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">
|
<span class="text-sm font-mono">
|
||||||
{AprsmeWeb.StatusLive.Index.format_number(@aprs_status.stored_packet_count)}
|
{AprsmeWeb.StatusLive.Index.format_number(@aprs_status.stored_packet_count)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -113,19 +117,24 @@ defmodule AprsmeWeb.StatusLive.ConnectionCardComponent do
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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="flex items-center">
|
||||||
<div class="rating rating-sm mr-2">
|
<div class="flex items-center mr-2">
|
||||||
<%= for i <- 1..5 do %>
|
<%= for i <- 1..5 do %>
|
||||||
<input
|
<svg
|
||||||
type="radio"
|
class={"h-4 w-4 #{if i <= @health_score, do: "text-amber-400", else: "text-gray-300 dark:text-gray-600"}"}
|
||||||
name="health-rating"
|
viewBox="0 0 20 20"
|
||||||
class="mask mask-star-2 bg-orange-400"
|
fill="currentColor"
|
||||||
disabled
|
aria-hidden="true"
|
||||||
checked={i <= @health_score}
|
>
|
||||||
/>
|
<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 %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm font-medium">
|
<span class="text-sm font-medium">
|
||||||
|
|
@ -133,7 +142,7 @@ defmodule AprsmeWeb.StatusLive.ConnectionCardComponent do
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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)}
|
{AprsmeWeb.StatusLive.Index.get_health_description(@health_score, @aprs_status.connected)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -101,38 +101,41 @@ defmodule AprsmeWeb.StatusLive.Index do
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="container mx-auto max-w-4xl p-4">
|
<div class="mx-auto max-w-4xl p-4 sm:px-6 lg:px-8">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-8">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h1 class="card-title text-3xl">{gettext("APRS.me System Status")}</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{gettext("APRS.me System Status")}</h1>
|
||||||
<div class="text-sm opacity-70">
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{gettext("Last updated:")}
|
{gettext("Last updated:")}
|
||||||
<span class="font-mono">{Calendar.strftime(@current_time, "%H:%M:%S UTC")}</span>
|
<span class="font-mono">{Calendar.strftime(@current_time, "%H:%M:%S UTC")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Overall Status Alert -->
|
<!-- Overall Status Alert -->
|
||||||
<%= if not @aprs_status.connected do %>
|
<%= if not @aprs_status.connected do %>
|
||||||
<div class="alert alert-error mb-6">
|
<div class="rounded-md bg-red-50 p-4 mb-6 dark:bg-red-500/10">
|
||||||
<svg
|
<div class="flex">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
class="h-5 w-5 text-red-400 shrink-0"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
>
|
||||||
stroke-linejoin="round"
|
<path
|
||||||
stroke-width="2"
|
stroke-linecap="round"
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
stroke-linejoin="round"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
<div>
|
/>
|
||||||
<h3 class="font-bold">{gettext("APRS-IS Connection Issue")}</h3>
|
</svg>
|
||||||
<div class="text-xs">
|
<div class="ml-3">
|
||||||
{gettext(
|
<h3 class="text-sm font-medium text-red-800 dark:text-red-300">{gettext("APRS-IS Connection Issue")}</h3>
|
||||||
"The system is currently disconnected from the APRS-IS network. This may be due to network issues or server maintenance."
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -140,239 +143,254 @@ defmodule AprsmeWeb.StatusLive.Index do
|
||||||
|
|
||||||
<!-- APRS-IS Connection Status -->
|
<!-- APRS-IS Connection Status -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4">{gettext("APRS-IS Connection")}</h2>
|
<h2 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">{gettext("APRS-IS Connection")}</h2>
|
||||||
<div class="card bg-base-200">
|
<div class="rounded-lg bg-gray-50 p-6 dark:bg-gray-700/50">
|
||||||
<div class="card-body">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="flex items-center">
|
||||||
<div class="flex items-center">
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Status:")}</span>
|
||||||
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Status:")}</span>
|
<%= if @aprs_status.connected do %>
|
||||||
<%= 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">
|
||||||
<div class="badge badge-success gap-1">
|
<span class="h-2 w-2 rounded-full bg-current"></span>
|
||||||
<div class="w-2 h-2 bg-current rounded-full"></div>
|
{gettext("Connected")}
|
||||||
{gettext("Connected")}
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<div class="badge badge-error gap-1">
|
|
||||||
<div class="w-2 h-2 bg-current rounded-full"></div>
|
|
||||||
{gettext("Disconnected")}
|
|
||||||
</div>
|
|
||||||
<% 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>
|
</span>
|
||||||
</div>
|
<% 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">
|
||||||
<div class="flex items-center">
|
<span class="h-2 w-2 rounded-full bg-current"></span>
|
||||||
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Login ID:")}</span>
|
{gettext("Disconnected")}
|
||||||
<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>
|
||||||
<span class="text-sm">
|
<% end %>
|
||||||
<%= 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>
|
|
||||||
</div>
|
</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 %>
|
||||||
|
<span class="text-gray-400 dark:text-gray-500">{gettext("Not connected")}</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("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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Packet Statistics -->
|
<!-- Packet Statistics -->
|
||||||
<div class="divider"></div>
|
<div class="border-t border-gray-200 dark:border-white/10 my-4"></div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium mb-3">{gettext("Packet Statistics")}</h3>
|
<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="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>
|
|
||||||
|
|
||||||
<!-- 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="flex items-center">
|
||||||
<div class="rating rating-sm mr-2">
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
|
||||||
<%= for i <- 1..5 do %>
|
{gettext("Total Packets:")}
|
||||||
<input
|
</span>
|
||||||
type="radio"
|
<span class="text-sm font-mono text-gray-900 dark:text-white">
|
||||||
name="health-rating"
|
{format_number(@aprs_status.packet_stats.total_packets)}
|
||||||
class="mask mask-star-2 bg-orange-400"
|
</span>
|
||||||
disabled
|
</div>
|
||||||
checked={i <= @health_score}
|
|
||||||
/>
|
<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 %>
|
<% end %>
|
||||||
</div>
|
|
||||||
<span class="text-sm font-medium">
|
|
||||||
{@health_score}/5
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs opacity-70 mt-1">
|
|
||||||
{get_health_description(@health_score, @aprs_status.connected)}
|
|
||||||
</p>
|
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cluster Information (if available) -->
|
<!-- Cluster Information (if available) -->
|
||||||
<%= if Map.has_key?(@aprs_status, :cluster_info) do %>
|
<%= if Map.has_key?(@aprs_status, :cluster_info) do %>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4">{gettext("Cluster Status")}</h2>
|
<h2 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">{gettext("Cluster Status")}</h2>
|
||||||
<div class="card bg-base-200">
|
<div class="rounded-lg bg-gray-50 p-6 dark:bg-gray-700/50">
|
||||||
<div class="card-body">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="flex items-center">
|
||||||
<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-medium opacity-70 mr-2">{gettext("Total Nodes:")}</span>
|
<span class="text-sm font-mono text-gray-900 dark:text-white">
|
||||||
<span class="text-sm font-mono">{@aprs_status.cluster_info.total_nodes}</span>
|
{@aprs_status.cluster_info.total_nodes}
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Cluster Mode:")}</span>
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">
|
||||||
<div class="badge badge-success gap-1">
|
{gettext("Connected Nodes:")}
|
||||||
<div class="w-2 h-2 bg-current rounded-full"></div>
|
</span>
|
||||||
{gettext("Enabled")}
|
<span class="text-sm font-mono text-gray-900 dark:text-white">
|
||||||
</div>
|
{@aprs_status.cluster_info.connected_nodes}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if Map.has_key?(@aprs_status.cluster_info, :all_nodes) do %>
|
<div class="flex items-center">
|
||||||
<div class="mt-4">
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-2">{gettext("Leader Node:")}</span>
|
||||||
<span class="text-sm font-medium opacity-70">{gettext("Cluster Nodes:")}</span>
|
<span class="text-sm font-mono text-gray-900 dark:text-white">
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
{@aprs_status.cluster_info.leader_node}
|
||||||
<%= for node_name <- @aprs_status.cluster_info.all_nodes do %>
|
</span>
|
||||||
<div class={[
|
</div>
|
||||||
"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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -6,51 +6,66 @@ defmodule AprsmeWeb.UserConfirmationInstructionsLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div class="hero-content text-center">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div class="max-w-md">
|
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
Resend confirmation instructions
|
||||||
<div class="card-body">
|
</h2>
|
||||||
<h1 class="card-title text-3xl mb-4 justify-center">
|
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
|
||||||
Resend confirmation instructions
|
Enter your email address to receive a new confirmation link
|
||||||
</h1>
|
</p>
|
||||||
<p class="text-base-content/70 mb-6">
|
</div>
|
||||||
Enter your email address to receive a new confirmation link
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<.simple_form :let={_f} for={%{}} as={:user} id="resend_confirmation_form" phx-submit="send_instructions">
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||||
<div class="form-control w-full">
|
<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">
|
||||||
<label class="label">
|
<.simple_form :let={_f} for={%{}} as={:user} id="resend_confirmation_form" phx-submit="send_instructions">
|
||||||
<span class="label-text">Email</span>
|
<div>
|
||||||
</label>
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Email</label>
|
||||||
<input
|
<div class="mt-2">
|
||||||
type="email"
|
<input
|
||||||
name="user[email]"
|
type="email"
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
name="user[email]"
|
||||||
placeholder="Enter your 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"
|
||||||
required
|
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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,36 +6,55 @@ defmodule AprsmeWeb.UserConfirmationLive do
|
||||||
|
|
||||||
def render(%{live_action: :edit} = assigns) do
|
def render(%{live_action: :edit} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div class="hero-content text-center">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div class="max-w-md">
|
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
Confirm your account
|
||||||
<div class="card-body">
|
</h2>
|
||||||
<h1 class="card-title text-3xl mb-4 justify-center">Confirm your account</h1>
|
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-base-content/70 mb-6">Click the button below to confirm your account</p>
|
Click the button below to confirm your account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<.simple_form :let={_f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account">
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||||
<input type="hidden" name="user[token]" value={@token} />
|
<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">
|
<div>
|
||||||
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Confirming...">
|
<button
|
||||||
Confirm my account
|
type="submit"
|
||||||
</button>
|
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"
|
||||||
</div>
|
phx-disable-with="Confirming..."
|
||||||
</.simple_form>
|
>
|
||||||
|
Confirm my account
|
||||||
<div class="divider">OR</div>
|
</button>
|
||||||
|
|
||||||
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,47 +6,66 @@ defmodule AprsmeWeb.UserForgotPasswordLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div class="hero-content text-center">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div class="max-w-md">
|
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
Forgot your password?
|
||||||
<div class="card-body">
|
</h2>
|
||||||
<h1 class="card-title text-3xl mb-4 justify-center">Forgot your password?</h1>
|
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-base-content/70 mb-6">We'll send a password reset link to your inbox</p>
|
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="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||||
<div class="form-control w-full">
|
<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">
|
||||||
<label class="label">
|
<.simple_form :let={_f} id="reset_password_form" for={%{}} as={:user} phx-submit="send_email">
|
||||||
<span class="label-text">Email</span>
|
<div>
|
||||||
</label>
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Email</label>
|
||||||
<input
|
<div class="mt-2">
|
||||||
type="email"
|
<input
|
||||||
name="user[email]"
|
type="email"
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
name="user[email]"
|
||||||
placeholder="Enter your 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"
|
||||||
required
|
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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,82 +7,100 @@ defmodule AprsmeWeb.UserLoginLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div class="hero-content text-center">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div class="max-w-md">
|
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
{gettext("Sign in to account")}
|
||||||
<div class="card-body">
|
</h2>
|
||||||
<h1 class="card-title text-3xl mb-4 justify-center">{gettext("Sign in to account")}</h1>
|
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-base-content/70 mb-6">
|
{gettext("Don't have an account?")}
|
||||||
{gettext("Don't have an account?")}
|
<.link
|
||||||
<.link navigate={~p"/users/register"} class="link link-primary">
|
navigate={~p"/users/register"}
|
||||||
{gettext("Sign up")}
|
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
</.link>
|
>
|
||||||
{gettext("for an account now.")}
|
{gettext("Sign up")}
|
||||||
</p>
|
</.link>
|
||||||
|
{gettext("for an account now.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<.simple_form
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||||
:let={f}
|
<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">
|
||||||
id="login_form"
|
<.simple_form
|
||||||
for={@changeset}
|
:let={f}
|
||||||
action={~p"/users/log_in"}
|
id="login_form"
|
||||||
as={:user}
|
for={@changeset}
|
||||||
phx-update="ignore"
|
action={~p"/users/log_in"}
|
||||||
>
|
as={:user}
|
||||||
<div class="form-control w-full">
|
phx-update="ignore"
|
||||||
<label class="label">
|
>
|
||||||
<span class="label-text">{gettext("Email")}</span>
|
<div>
|
||||||
</label>
|
<label for="user_email" class="block text-sm/6 font-medium text-gray-900 dark:text-white">
|
||||||
<input
|
{gettext("Email")}
|
||||||
type="email"
|
</label>
|
||||||
name={Phoenix.HTML.Form.input_name(f, :email)}
|
<div class="mt-2">
|
||||||
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
|
<input
|
||||||
class="input input-bordered w-full"
|
type="email"
|
||||||
placeholder={gettext("Enter your email")}
|
id="user_email"
|
||||||
required
|
name={Phoenix.HTML.Form.input_name(f, :email)}
|
||||||
/>
|
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
|
||||||
</div>
|
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")}
|
||||||
<div class="form-control w-full">
|
required
|
||||||
<label class="label">
|
/>
|
||||||
<span class="label-text">{gettext("Password")}</span>
|
</div>
|
||||||
</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,109 +9,130 @@ defmodule AprsmeWeb.UserRegistrationLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div class="hero-content text-center">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div class="max-w-md">
|
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
{gettext("Register for an account")}
|
||||||
<div class="card-body">
|
</h2>
|
||||||
<h1 class="card-title text-3xl mb-4 justify-center">{gettext("Register for an account")}</h1>
|
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-base-content/70 mb-6">
|
{gettext("Already registered?")}
|
||||||
{gettext("Already registered?")}
|
<.link
|
||||||
<.link navigate={~p"/users/log_in"} class="link link-primary">
|
navigate={~p"/users/log_in"}
|
||||||
{gettext("Sign in")}
|
class="font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
</.link>
|
>
|
||||||
{gettext("to your account now.")}
|
{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>
|
</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>
|
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,72 +6,93 @@ defmodule AprsmeWeb.UserResetPasswordLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div class="hero-content text-center">
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div class="max-w-md">
|
<h2 class="text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
Reset Password
|
||||||
<div class="card-body">
|
</h2>
|
||||||
<h1 class="card-title text-3xl mb-4 justify-center">Reset Password</h1>
|
<p class="mt-2 text-center text-sm/6 text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-base-content/70 mb-6">Enter your new password below</p>
|
Enter your new password below
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<.simple_form
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||||
:let={f}
|
<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">
|
||||||
for={@changeset}
|
<.simple_form
|
||||||
id="reset_password_form"
|
:let={f}
|
||||||
phx-submit="reset_password"
|
for={@changeset}
|
||||||
phx-change="validate"
|
id="reset_password_form"
|
||||||
>
|
phx-submit="reset_password"
|
||||||
<div :if={@changeset.action == :insert} class="alert alert-error mb-4">
|
phx-change="validate"
|
||||||
<span>Oops, something went wrong! Please check the errors below.</span>
|
>
|
||||||
</div>
|
<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">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">New password</label>
|
||||||
<span class="label-text">New password</span>
|
<div class="mt-2">
|
||||||
</label>
|
<input
|
||||||
<input
|
type="password"
|
||||||
type="password"
|
name={Phoenix.HTML.Form.input_name(f, :password)}
|
||||||
name={Phoenix.HTML.Form.input_name(f, :password)}
|
value={Phoenix.HTML.Form.input_value(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"
|
||||||
class="input input-bordered w-full bg-base-100 text-base-content"
|
placeholder="Enter new password"
|
||||||
placeholder="Enter new password"
|
required
|
||||||
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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,250 +8,289 @@ defmodule AprsmeWeb.UserSettingsLive do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="min-h-screen bg-gray-50 py-12 dark:bg-gray-900">
|
||||||
<div class="hero-content flex-col w-full max-w-4xl">
|
<div class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<h1 class="text-4xl font-bold">Account Settings</h1>
|
<h1 class="text-4xl font-bold text-gray-900 dark:text-white">Account Settings</h1>
|
||||||
<p class="text-base-content/70">Update your email address, callsign, and password</p>
|
<p class="mt-2 text-gray-500 dark:text-gray-400">Update your email address, callsign, and password</p>
|
||||||
</div>
|
</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 -->
|
<!-- Change Email Section -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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">
|
||||||
<div class="card-body">
|
<h2 class="text-xl font-semibold text-gray-900 mb-6 dark:text-white">Change Email</h2>
|
||||||
<h2 class="card-title text-2xl mb-4">Change Email</h2>
|
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
:let={f}
|
:let={f}
|
||||||
id="email_form"
|
id="email_form"
|
||||||
for={@email_changeset}
|
for={@email_changeset}
|
||||||
phx-submit="update_email"
|
phx-submit="update_email"
|
||||||
phx-change="validate_email"
|
phx-change="validate_email"
|
||||||
>
|
>
|
||||||
<div :if={@email_changeset.action == :insert} class="alert alert-error mb-4">
|
<div :if={@email_changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
|
||||||
<span>Oops, something went wrong! Please check the errors below.</span>
|
<p class="text-sm text-red-700 dark:text-red-400">
|
||||||
</div>
|
Oops, something went wrong! Please check the errors below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-control w-full">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Email</label>
|
||||||
<span class="label-text">Email</span>
|
<div class="mt-2">
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name={Phoenix.HTML.Form.input_name(f, :email)}
|
name={Phoenix.HTML.Form.input_name(f, :email)}
|
||||||
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
|
value={Phoenix.HTML.Form.input_value(f, :email) || ""}
|
||||||
class={[
|
class={[
|
||||||
"input input-bordered w-full bg-base-100 text-base-content",
|
"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",
|
||||||
Keyword.has_key?(@email_changeset.errors, :email) && "input-error"
|
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"
|
placeholder="Enter your email"
|
||||||
required
|
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>
|
</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">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Current password</label>
|
||||||
<span class="label-text">Current password</span>
|
<div class="mt-2">
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password_for_email"
|
id="current_password_for_email"
|
||||||
value={@email_form_current_password || ""}
|
value={@email_form_current_password || ""}
|
||||||
class={[
|
class={[
|
||||||
"input input-bordered w-full bg-base-100 text-base-content",
|
"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",
|
||||||
Keyword.has_key?(@email_changeset.errors, :current_password) && "input-error"
|
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"
|
placeholder="Enter current password"
|
||||||
required
|
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>
|
</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">
|
<div>
|
||||||
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Changing...">
|
<button
|
||||||
Change Email
|
type="submit"
|
||||||
</button>
|
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"
|
||||||
</div>
|
phx-disable-with="Changing..."
|
||||||
</.simple_form>
|
>
|
||||||
</div>
|
Change Email
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</.simple_form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Callsign Section -->
|
<!-- Change Callsign Section -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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">
|
||||||
<div class="card-body">
|
<h2 class="text-xl font-semibold text-gray-900 mb-6 dark:text-white">Change Callsign</h2>
|
||||||
<h2 class="card-title text-2xl mb-4">Change Callsign</h2>
|
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
:let={f}
|
:let={f}
|
||||||
id="callsign_form"
|
id="callsign_form"
|
||||||
for={@callsign_changeset}
|
for={@callsign_changeset}
|
||||||
phx-submit="update_callsign"
|
phx-submit="update_callsign"
|
||||||
phx-change="validate_callsign"
|
phx-change="validate_callsign"
|
||||||
>
|
>
|
||||||
<div :if={@callsign_changeset.action == :insert} class="alert alert-error mb-4">
|
<div :if={@callsign_changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
|
||||||
<span>Oops, something went wrong! Please check the errors below.</span>
|
<p class="text-sm text-red-700 dark:text-red-400">
|
||||||
</div>
|
Oops, something went wrong! Please check the errors below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-control w-full">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Callsign</label>
|
||||||
<span class="label-text">Callsign</span>
|
<div class="mt-2">
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name={Phoenix.HTML.Form.input_name(f, :callsign)}
|
name={Phoenix.HTML.Form.input_name(f, :callsign)}
|
||||||
value={Phoenix.HTML.Form.input_value(f, :callsign) || ""}
|
value={Phoenix.HTML.Form.input_value(f, :callsign) || ""}
|
||||||
class={[
|
class={[
|
||||||
"input input-bordered w-full bg-base-100 text-base-content",
|
"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",
|
||||||
Keyword.has_key?(@callsign_changeset.errors, :callsign) && "input-error"
|
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"
|
placeholder="Enter your callsign"
|
||||||
required
|
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>
|
</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">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Current password</label>
|
||||||
<span class="label-text">Current password</span>
|
<div class="mt-2">
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password_for_callsign"
|
id="current_password_for_callsign"
|
||||||
value={@callsign_form_current_password || ""}
|
value={@callsign_form_current_password || ""}
|
||||||
class={[
|
class={[
|
||||||
"input input-bordered w-full bg-base-100 text-base-content",
|
"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",
|
||||||
Keyword.has_key?(@callsign_changeset.errors, :current_password) && "input-error"
|
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"
|
placeholder="Enter current password"
|
||||||
required
|
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>
|
</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">
|
<div>
|
||||||
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Changing...">
|
<button
|
||||||
Change Callsign
|
type="submit"
|
||||||
</button>
|
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"
|
||||||
</div>
|
phx-disable-with="Changing..."
|
||||||
</.simple_form>
|
>
|
||||||
</div>
|
Change Callsign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</.simple_form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Password Section -->
|
<!-- Change Password Section -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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">
|
||||||
<div class="card-body">
|
<h2 class="text-xl font-semibold text-gray-900 mb-6 dark:text-white">Change Password</h2>
|
||||||
<h2 class="card-title text-2xl mb-4">Change Password</h2>
|
|
||||||
|
|
||||||
<.simple_form
|
<.simple_form
|
||||||
:let={f}
|
:let={f}
|
||||||
id="password_form"
|
id="password_form"
|
||||||
for={@password_changeset}
|
for={@password_changeset}
|
||||||
action={~p"/users/log_in?_action=password_updated"}
|
action={~p"/users/log_in?_action=password_updated"}
|
||||||
method="post"
|
method="post"
|
||||||
phx-change="validate_password"
|
phx-change="validate_password"
|
||||||
phx-submit="update_password"
|
phx-submit="update_password"
|
||||||
phx-trigger-action={@trigger_submit}
|
phx-trigger-action={@trigger_submit}
|
||||||
>
|
>
|
||||||
<div :if={@password_changeset.action == :insert} class="alert alert-error mb-4">
|
<div :if={@password_changeset.action == :insert} class="rounded-md bg-red-50 p-4 dark:bg-red-500/10">
|
||||||
<span>Oops, something went wrong! Please check the errors below.</span>
|
<p class="text-sm text-red-700 dark:text-red-400">
|
||||||
</div>
|
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">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">New password</label>
|
||||||
<span class="label-text">New password</span>
|
<div class="mt-2">
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name={Phoenix.HTML.Form.input_name(f, :password)}
|
name={Phoenix.HTML.Form.input_name(f, :password)}
|
||||||
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
|
value={Phoenix.HTML.Form.input_value(f, :password) || ""}
|
||||||
class={[
|
class={[
|
||||||
"input input-bordered w-full bg-base-100 text-base-content",
|
"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",
|
||||||
Keyword.has_key?(@password_changeset.errors, :password) && "input-error"
|
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"
|
placeholder="Enter new password"
|
||||||
required
|
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>
|
</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">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Confirm new password</label>
|
||||||
<span class="label-text">Confirm new password</span>
|
<div class="mt-2">
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name={Phoenix.HTML.Form.input_name(f, :password_confirmation)}
|
name={Phoenix.HTML.Form.input_name(f, :password_confirmation)}
|
||||||
value={Phoenix.HTML.Form.input_value(f, :password_confirmation) || ""}
|
value={Phoenix.HTML.Form.input_value(f, :password_confirmation) || ""}
|
||||||
class={[
|
class={[
|
||||||
"input input-bordered w-full bg-base-100 text-base-content",
|
"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",
|
||||||
Keyword.has_key?(@password_changeset.errors, :password_confirmation) &&
|
if(Keyword.has_key?(@password_changeset.errors, :password_confirmation),
|
||||||
"input-error"
|
do: "outline-red-500 dark:outline-red-400",
|
||||||
|
else: "outline-gray-300 dark:outline-white/10"
|
||||||
|
)
|
||||||
]}
|
]}
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
required
|
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>
|
</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">
|
<div>
|
||||||
<label class="label">
|
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white">Current password</label>
|
||||||
<span class="label-text">Current password</span>
|
<div class="mt-2">
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password_for_password"
|
id="current_password_for_password"
|
||||||
value={@current_password || ""}
|
value={@current_password || ""}
|
||||||
class={[
|
class={[
|
||||||
"input input-bordered w-full bg-base-100 text-base-content",
|
"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",
|
||||||
Keyword.has_key?(@password_changeset.errors, :current_password) && "input-error"
|
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"
|
placeholder="Enter current password"
|
||||||
required
|
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>
|
</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">
|
<div>
|
||||||
<button type="submit" class="btn btn-primary w-full" phx-disable-with="Changing...">
|
<button
|
||||||
Change Password
|
type="submit"
|
||||||
</button>
|
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"
|
||||||
</div>
|
phx-disable-with="Changing..."
|
||||||
</.simple_form>
|
>
|
||||||
</div>
|
Change Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</.simple_form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<%= if @weather_packet do %>
|
<%= if @weather_packet do %>
|
||||||
<div class="bg-base-200">
|
<div class="bg-gray-50 dark:bg-gray-900">
|
||||||
<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="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="py-4 md:flex md:items-center md:justify-between">
|
<div class="py-4 md:flex md:items-center md:justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
{gettext("Weather Station")}
|
{gettext("Weather Station")}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="ml-3 flex items-center space-x-3">
|
<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}
|
{@callsign}
|
||||||
</span>
|
</span>
|
||||||
<% {symbol_table_id, symbol_code} =
|
<% {symbol_table_id, symbol_code} =
|
||||||
|
|
@ -39,13 +39,22 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex md:ml-4 md:mt-0">
|
<div class="mt-3 flex md:ml-4 md:mt-0">
|
||||||
<div class="flex space-x-2">
|
<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")}
|
{gettext("View info")}
|
||||||
</.link>
|
</.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")}
|
{gettext("View packets")}
|
||||||
</.link>
|
</.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")}
|
{gettext("View on map")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,15 +63,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
|
<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="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="card-body">
|
<div class="px-6 py-6">
|
||||||
<% dt =
|
<% dt =
|
||||||
AprsmeWeb.TimeHelpers.to_datetime(
|
AprsmeWeb.TimeHelpers.to_datetime(
|
||||||
Map.get(@weather_packet, :inserted_at) || Map.get(@weather_packet, "inserted_at")
|
Map.get(@weather_packet, :inserted_at) || Map.get(@weather_packet, "inserted_at")
|
||||||
) %>
|
) %>
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="flex-shrink-0">
|
<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
|
<path
|
||||||
fill-rule="evenodd"
|
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"
|
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>
|
</div>
|
||||||
<dl class="grid grid-cols-2 gap-3">
|
<dl class="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
<%= if dt do %>
|
<%= if dt do %>
|
||||||
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(dt)})<br />
|
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(dt)})<br />
|
||||||
|
|
@ -88,7 +102,7 @@
|
||||||
</div>
|
</div>
|
||||||
<%= if has_weather_field?(@weather_packet, :temperature) do %>
|
<%= if has_weather_field?(@weather_packet, :temperature) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{format_weather_value(@weather_packet, :temperature, @locale)}
|
{format_weather_value(@weather_packet, :temperature, @locale)}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
@ -96,7 +110,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if has_weather_field?(@weather_packet, :humidity) do %>
|
<%= if has_weather_field?(@weather_packet, :humidity) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
|
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
|
||||||
</dd>
|
</dd>
|
||||||
|
|
@ -104,7 +118,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if has_weather_field?(@weather_packet, :wind_direction) || has_weather_field?(@weather_packet, :wind_speed) do %>
|
<%= if has_weather_field?(@weather_packet, :wind_direction) || has_weather_field?(@weather_packet, :wind_speed) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
<%= if has_weather_field?(@weather_packet, :wind_direction) do %>
|
<%= if has_weather_field?(@weather_packet, :wind_direction) do %>
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}°
|
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}°
|
||||||
|
|
@ -117,7 +131,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if has_weather_field?(@weather_packet, :wind_gust) do %>
|
<%= if has_weather_field?(@weather_packet, :wind_gust) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{format_weather_value(@weather_packet, :wind_gust, @locale)}
|
{format_weather_value(@weather_packet, :wind_gust, @locale)}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
@ -125,7 +139,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if has_weather_field?(@weather_packet, :pressure) do %>
|
<%= if has_weather_field?(@weather_packet, :pressure) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
|
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
|
||||||
</dd>
|
</dd>
|
||||||
|
|
@ -133,7 +147,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if has_weather_field?(@weather_packet, :rain_1h) do %>
|
<%= if has_weather_field?(@weather_packet, :rain_1h) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{format_weather_value(@weather_packet, :rain_1h, @locale)}
|
{format_weather_value(@weather_packet, :rain_1h, @locale)}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
@ -141,7 +155,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if has_weather_field?(@weather_packet, :rain_24h) do %>
|
<%= if has_weather_field?(@weather_packet, :rain_24h) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{format_weather_value(@weather_packet, :rain_24h, @locale)}
|
{format_weather_value(@weather_packet, :rain_24h, @locale)}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
@ -149,14 +163,14 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if has_weather_field?(@weather_packet, :rain_since_midnight) do %>
|
<%= if has_weather_field?(@weather_packet, :rain_since_midnight) do %>
|
||||||
<div>
|
<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">
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
{format_weather_value(@weather_packet, :rain_since_midnight, @locale)}
|
{format_weather_value(@weather_packet, :rain_since_midnight, @locale)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="col-span-2">
|
<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">
|
<dd class="mt-1 text-xs font-mono break-all">
|
||||||
<%= if is_binary(@weather_packet.raw_packet) do %>
|
<%= if is_binary(@weather_packet.raw_packet) do %>
|
||||||
{Aprsme.EncodingUtils.sanitize_string(@weather_packet.raw_packet)}
|
{Aprsme.EncodingUtils.sanitize_string(@weather_packet.raw_packet)}
|
||||||
|
|
@ -166,14 +180,14 @@
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</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 ||
|
<strong>{gettext("Raw comment")}:</strong> {@weather_packet.comment ||
|
||||||
@weather_packet["comment"]}
|
@weather_packet["comment"]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<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="card-body">
|
<div class="px-6 py-6">
|
||||||
<h3 class="text-sm font-medium mb-2">{gettext("Weather History Graphs")}</h3>
|
<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 %>
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :temperature, nil)))) do %>
|
||||||
<div
|
<div
|
||||||
|
|
@ -181,7 +195,7 @@
|
||||||
phx-hook="ChartJSTempChart"
|
phx-hook="ChartJSTempChart"
|
||||||
data-weather-history={@weather_history_json}
|
data-weather-history={@weather_history_json}
|
||||||
data-chart-labels={Jason.encode!(@chart_labels)}
|
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;"
|
style="height:250px;"
|
||||||
>
|
>
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
|
|
@ -193,7 +207,7 @@
|
||||||
phx-hook="ChartJSHumidityChart"
|
phx-hook="ChartJSHumidityChart"
|
||||||
data-weather-history={@weather_history_json}
|
data-weather-history={@weather_history_json}
|
||||||
data-chart-labels={Jason.encode!(@chart_labels)}
|
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;"
|
style="height:250px;"
|
||||||
>
|
>
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
|
|
@ -205,7 +219,7 @@
|
||||||
phx-hook="ChartJSPressureChart"
|
phx-hook="ChartJSPressureChart"
|
||||||
data-weather-history={@weather_history_json}
|
data-weather-history={@weather_history_json}
|
||||||
data-chart-labels={Jason.encode!(@chart_labels)}
|
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;"
|
style="height:250px;"
|
||||||
>
|
>
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
|
|
@ -217,7 +231,7 @@
|
||||||
phx-hook="ChartJSWindChart"
|
phx-hook="ChartJSWindChart"
|
||||||
data-weather-history={@weather_history_json}
|
data-weather-history={@weather_history_json}
|
||||||
data-chart-labels={Jason.encode!(@chart_labels)}
|
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;"
|
style="height:250px;"
|
||||||
>
|
>
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
|
|
@ -229,7 +243,7 @@
|
||||||
phx-hook="ChartJSRainChart"
|
phx-hook="ChartJSRainChart"
|
||||||
data-weather-history={@weather_history_json}
|
data-weather-history={@weather_history_json}
|
||||||
data-chart-labels={Jason.encode!(@chart_labels)}
|
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;"
|
style="height:250px;"
|
||||||
>
|
>
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
|
|
@ -241,7 +255,7 @@
|
||||||
phx-hook="ChartJSLuminosityChart"
|
phx-hook="ChartJSLuminosityChart"
|
||||||
data-weather-history={@weather_history_json}
|
data-weather-history={@weather_history_json}
|
||||||
data-chart-labels={Jason.encode!(@chart_labels)}
|
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;"
|
style="height:250px;"
|
||||||
>
|
>
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
|
|
@ -253,11 +267,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="bg-base-200 flex items-center justify-center min-h-screen">
|
<div class="bg-gray-50 dark:bg-gray-900 flex items-center justify-center min-h-screen">
|
||||||
<div class="card bg-base-100 shadow-xl max-w-lg">
|
<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="card-body text-center">
|
<div class="px-6 py-8 text-center">
|
||||||
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,14 @@ defmodule AprsmeWeb.BadPacketsLiveTest do
|
||||||
alias Aprsme.Repo
|
alias Aprsme.Repo
|
||||||
|
|
||||||
describe "Index" do
|
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)
|
{:ok, _index_live, html} = live(conn, ~p"/badpackets", on_error: :warn)
|
||||||
|
|
||||||
assert html =~ "card"
|
assert html =~ "shadow-sm"
|
||||||
assert html =~ "bg-base-100"
|
assert html =~ "sm:rounded-lg"
|
||||||
assert html =~ "shadow-xl"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "lists all bad packets with DaisyUI table", %{conn: conn} do
|
test "lists all bad packets with table", %{conn: conn} do
|
||||||
bad_packet =
|
bad_packet =
|
||||||
Repo.insert!(%BadPacket{
|
Repo.insert!(%BadPacket{
|
||||||
raw_packet: "KD9PDP>APRS:Invalid packet data",
|
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_message
|
||||||
assert html =~ bad_packet.error_type
|
assert html =~ bad_packet.error_type
|
||||||
assert html =~ "table"
|
assert html =~ "table"
|
||||||
assert html =~ "badge"
|
assert html =~ "rounded-md"
|
||||||
end
|
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)
|
{:ok, _index_live, html} = live(conn, ~p"/badpackets", on_error: :warn)
|
||||||
|
|
||||||
assert html =~ "No bad packets"
|
assert html =~ "No bad packets"
|
||||||
assert html =~ "All packets are parsing successfully!"
|
assert html =~ "All packets are parsing successfully!"
|
||||||
assert html =~ "text-success"
|
assert html =~ "text-green-600"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates in real-time when new bad packet is created", %{conn: conn} do
|
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
|
assert html =~ bad_packet.error_type
|
||||||
end
|
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)
|
{:ok, _index_live, html} = live(conn, ~p"/badpackets", on_error: :warn)
|
||||||
|
|
||||||
# DaisyUI uses data-theme for theming
|
assert html =~ "dark:bg-gray-800"
|
||||||
# The components should work with both light and dark themes
|
assert html =~ "dark:text-gray-400"
|
||||||
assert html =~ "bg-base-100"
|
|
||||||
assert html =~ "text-base-content"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue