Profile page:
* New MicrowavepropWeb.UserProfileLive at /u/:callsign is a public
page showing a user's contacts and beacons. Resolves case-
insensitively so /u/w5isp and /u/W5ISP are the same thing; unknown
callsigns redirect to /. Uses daisyUI card / stats / table
components with an avatar-placeholder initial and hero icons.
* Accounts.get_user_by_callsign/1 (case-insensitive) plus
Radio.list_contacts_for_user/1 and Beacons.list_beacons_for_user/1
back the page. Beacons list includes both approved and pending so
owners see their drafts.
* The top nav bar and the three LiveView sidebars (MapLive,
WeatherMapLive, ContactMapLive) now render the logged-in callsign
as a navigate link to /u/:callsign instead of a static label.
* Nine new tests cover the lookup, the LiveView render, and the
ownership-scoped queries.
Flexible band input:
* New Microwaveprop.Radio.BandResolver module converts any of:
ADIF wavelength labels ("33cm", "1.25cm", "6mm", case/whitespace
insensitive), numeric frequency strings ("903.100", "10368.000"),
and canonical MHz integers into the one of the site's known bands.
Returns the nearest allowed band for numeric inputs >= 900 MHz,
nil otherwise.
* 902 MHz is added to Contact.@allowed_bands, ContactEdit.@allowed_bands,
AdifImport.@allowed_bands, and the BandResolver list so "33cm"
round-trips end-to-end.
* AdifImport and CsvImport now delegate band resolution to
BandResolver, and Radio.create_contact/2 normalizes the :band attr
on the way in so the manual form and any API callers benefit too.
CsvImport's "invalid band" tests previously used 99999 MHz which
the new resolver snaps to the nearest allowed band; swapped to
"notaband" which is truly unresolvable.
Contacts and beacons list UX:
* Remove the "Submitted" column from /contacts — it duplicated info
already visible on the detail page and was pushing the real
columns off narrow viewports. submitted_cell/1 and its three
column-specific tests go with it.
* Hide the Lat / Lon columns from /beacons — six decimal places of
coordinates weren't useful next to the grid square and took a
disproportionate amount of row width.
202 lines
6.4 KiB
Elixir
202 lines
6.4 KiB
Elixir
defmodule MicrowavepropWeb.Layouts do
|
|
@moduledoc """
|
|
This module holds layouts and related functionality
|
|
used by your application.
|
|
"""
|
|
use MicrowavepropWeb, :html
|
|
|
|
# Embed all files in layouts/* within this module.
|
|
# The default root.html.heex file contains the HTML
|
|
# skeleton of your application, namely HTML headers
|
|
# and other static content.
|
|
embed_templates "layouts/*"
|
|
|
|
@doc """
|
|
Renders your app layout.
|
|
|
|
This function is typically invoked from every template,
|
|
and it often contains your application menu, sidebar,
|
|
or similar.
|
|
|
|
## Examples
|
|
|
|
<Layouts.app flash={@flash}>
|
|
<h1>Content</h1>
|
|
</Layouts.app>
|
|
|
|
"""
|
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|
|
|
attr :current_scope, :map,
|
|
default: nil,
|
|
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
|
|
|
|
attr :max_width, :string, default: "max-w-2xl", doc: "max width class for the content container"
|
|
slot :inner_block, required: true
|
|
|
|
def app(assigns) do
|
|
~H"""
|
|
<header class="navbar px-4 sm:px-6 lg:px-8">
|
|
<div class="flex-1 gap-4">
|
|
<a href="/" class="font-semibold">NTMS Propagation Prediction</a>
|
|
<nav class="flex gap-2">
|
|
<.link navigate="/map" class="btn btn-ghost btn-sm">Map</.link>
|
|
<.link navigate="/path" class="btn btn-ghost btn-sm">Path</.link>
|
|
<.link navigate="/beacons" class="btn btn-ghost btn-sm">Beacons</.link>
|
|
<.link navigate="/contacts/map" class="btn btn-ghost btn-sm">Contact Map</.link>
|
|
<.link navigate="/contacts" class="btn btn-ghost btn-sm">Contacts</.link>
|
|
<.link navigate="/submit" class="btn btn-ghost btn-sm">Submit</.link>
|
|
<.link navigate="/about" class="btn btn-ghost btn-sm">About</.link>
|
|
<.link
|
|
:if={@current_scope && @current_scope.user && @current_scope.user.is_admin}
|
|
navigate="/users"
|
|
class="btn btn-ghost btn-sm"
|
|
>
|
|
Users
|
|
</.link>
|
|
<.link
|
|
:if={@current_scope && @current_scope.user && @current_scope.user.is_admin}
|
|
navigate="/admin/contact-edits"
|
|
class="btn btn-ghost btn-sm"
|
|
>
|
|
Edits
|
|
</.link>
|
|
<.link
|
|
:if={@current_scope && @current_scope.user && @current_scope.user.is_admin}
|
|
href="/admin/oban"
|
|
class="btn btn-ghost btn-sm"
|
|
>
|
|
Oban
|
|
</.link>
|
|
<%= if @current_scope && @current_scope.user do %>
|
|
<.link
|
|
navigate={~p"/u/#{@current_scope.user.callsign}"}
|
|
class="btn btn-ghost btn-sm"
|
|
>
|
|
{@current_scope.user.callsign}
|
|
</.link>
|
|
<.link href={~p"/users/settings"} class="btn btn-ghost btn-sm">Settings</.link>
|
|
<.link href={~p"/users/log-out"} method="delete" class="btn btn-ghost btn-sm">
|
|
Log out
|
|
</.link>
|
|
<% else %>
|
|
<.link href={~p"/users/register"} class="btn btn-ghost btn-sm">Register</.link>
|
|
<.link href={~p"/users/log-in"} class="btn btn-ghost btn-sm">Log in</.link>
|
|
<% end %>
|
|
</nav>
|
|
</div>
|
|
<div class="flex-none">
|
|
<.theme_toggle />
|
|
</div>
|
|
</header>
|
|
|
|
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
|
<div class={["mx-auto space-y-4", @max_width]}>
|
|
{render_slot(@inner_block)}
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="px-4 sm:px-6 lg:px-8 pb-6">
|
|
<div class={["mx-auto text-center text-xs opacity-60 space-y-1", @max_width]}>
|
|
<div>
|
|
Built by and for the
|
|
<a href="https://ntms.org" target="_blank" class="link link-hover">
|
|
North Texas Microwave Society
|
|
</a>
|
|
</div>
|
|
<div>
|
|
Found this useful?
|
|
<a
|
|
href="https://www.paypal.com/ncp/payment/53VLBD2E67JAE"
|
|
target="_blank"
|
|
class="link link-hover"
|
|
>
|
|
Donate to NTMS
|
|
</a>
|
|
</div>
|
|
<div>
|
|
<.link navigate="/privacy" class="link link-hover">Privacy</.link>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<.flash_group flash={@flash} />
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Shows the flash group with standard titles and content.
|
|
|
|
## Examples
|
|
|
|
<.flash_group flash={@flash} />
|
|
"""
|
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
|
|
|
def flash_group(assigns) do
|
|
~H"""
|
|
<div id={@id} aria-live="polite">
|
|
<.flash kind={:info} flash={@flash} />
|
|
<.flash kind={:error} flash={@flash} />
|
|
|
|
<.flash
|
|
id="client-error"
|
|
kind={:error}
|
|
title={gettext("We can't find the internet")}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
|
|
<.flash
|
|
id="server-error"
|
|
kind={:error}
|
|
title={gettext("Something went wrong!")}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Provides dark vs light theme toggle based on themes defined in app.css.
|
|
|
|
See <head> in root.html.heex which applies the theme before page load.
|
|
"""
|
|
def theme_toggle(assigns) do
|
|
~H"""
|
|
<div class="card relative flex flex-row items-center border-2 border-base-300 bg-base-300 rounded-full">
|
|
<div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 transition-[left]" />
|
|
|
|
<button
|
|
class="flex p-2 cursor-pointer w-1/3"
|
|
phx-click={JS.dispatch("phx:set-theme")}
|
|
data-phx-theme="system"
|
|
>
|
|
<.icon name="hero-computer-desktop-micro" class="size-4 opacity-75 hover:opacity-100" />
|
|
</button>
|
|
|
|
<button
|
|
class="flex p-2 cursor-pointer w-1/3"
|
|
phx-click={JS.dispatch("phx:set-theme")}
|
|
data-phx-theme="light"
|
|
>
|
|
<.icon name="hero-sun-micro" class="size-4 opacity-75 hover:opacity-100" />
|
|
</button>
|
|
|
|
<button
|
|
class="flex p-2 cursor-pointer w-1/3"
|
|
phx-click={JS.dispatch("phx:set-theme")}
|
|
data-phx-theme="dark"
|
|
>
|
|
<.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
|
|
</button>
|
|
</div>
|
|
"""
|
|
end
|
|
end
|