/beacons gains a Leaflet map above the table showing every approved beacon as a circle marker with a popup that links to the detail page. New BeaconsListMap JS hook (assets/js/beacons_list_map_hook.ts) walks a data-beacons JSON payload, fitBounds over the marker set, and mirrors the format_freq/1 integer-comma formatting used on the detail view so the popups match the rest of the UI. Approved/on-air beacons render green, off-air beacons render gray. BeaconLive.Index.mount/3 now materializes the list into an assign so it can encode it into the data attribute; the live PubSub path keeps the map in sync on create/update/delete. Profile (/u/:callsign) polish: drop the hero-radio, hero-signal, and hero-information-circle icons per request, and replace the daisyUI `avatar placeholder` wrapper with a plain flex-centered circle so the initial letter actually lives inside the circle instead of anchored to the top-left corner (daisyUI 5 removed the old placeholder-centering behavior). Navigation ordering: in the top navbar (layouts.ex) Contacts now sits immediately before Contact Map instead of after it. All three vertical sidebars already had them adjacent in the correct order, so only the horizontal nav changed.
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" class="btn btn-ghost btn-sm">Contacts</.link>
|
|
<.link navigate="/contacts/map" class="btn btn-ghost btn-sm">Contact Map</.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
|