towerops/lib/towerops_web/components/layouts.ex

398 lines
14 KiB
Elixir

defmodule ToweropsWeb.Layouts do
@moduledoc """
This module holds layouts and related functionality
used by your application.
"""
use ToweropsWeb, :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)"
slot :inner_block, required: true
def app(assigns) do
~H"""
<div class="min-h-screen bg-zinc-50 dark:bg-zinc-950">
<header class="border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<nav class="flex h-16 items-center justify-between">
<div class="flex items-center">
<.link navigate={~p"/"} class="text-xl font-bold text-zinc-900 dark:text-zinc-100">
Towerops
</.link>
</div>
<div class="flex items-center gap-x-4">
<.link
navigate={~p"/users/log-in"}
class="text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
Log in
</.link>
<.link
navigate={~p"/users/register"}
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500"
>
Sign up
</.link>
</div>
</nav>
</div>
</header>
<main class="px-4 py-12 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
{render_slot(@inner_block)}
</div>
</main>
</div>
<.flash_group flash={@flash} />
"""
end
@doc """
Renders the authenticated app layout with navigation.
Use this for pages that require organization context.
## Examples
<Layouts.authenticated flash={@flash} current_organization={@current_organization} active_page="dashboard">
<h1>Content</h1>
</Layouts.authenticated>
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :current_scope, :map,
default: nil,
doc: "the current scope (contains user and impersonation state)"
attr :current_organization, :any,
default: nil,
doc: "the current organization (can be nil for pages without org context)"
attr :active_page, :string,
default: nil,
doc: "the currently active page (dashboard, sites, equipment, or alerts)"
slot :inner_block, required: true
def authenticated(assigns) do
~H"""
<div class="min-h-screen bg-zinc-50 dark:bg-zinc-950">
<!-- Impersonation Banner -->
<%= if @current_scope && @current_scope.impersonating? do %>
<div class="bg-yellow-400 border-b-2 border-yellow-600">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<span class="font-medium">
Impersonating: {@current_scope.user.email}
</span>
</div>
<.link
href={~p"/admin/impersonate"}
method="delete"
class="px-3 py-1 bg-white text-yellow-900 hover:bg-yellow-50 rounded font-medium text-sm"
>
Exit Impersonation
</.link>
</div>
</div>
</div>
<% end %>
<nav class="border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex">
<div class="flex flex-shrink-0 items-center">
<.link navigate={~p"/orgs"} class="text-xl font-bold text-zinc-900 dark:text-zinc-100">
Towerops
</.link>
</div>
<div :if={@current_organization} class="ml-10 flex space-x-8">
<.nav_link
navigate={~p"/orgs/#{@current_organization.slug}"}
active={@active_page == "dashboard"}
>
Dashboard
</.nav_link>
<.nav_link
navigate={~p"/orgs/#{@current_organization.slug}/sites"}
active={@active_page == "sites"}
>
Sites
</.nav_link>
<.nav_link
navigate={~p"/orgs/#{@current_organization.slug}/equipment"}
active={@active_page == "equipment"}
>
Equipment
</.nav_link>
<.nav_link
navigate={~p"/orgs/#{@current_organization.slug}/alerts"}
active={@active_page == "alerts"}
>
Alerts
</.nav_link>
</div>
</div>
<div class="relative flex items-center">
<button
type="button"
id="org-menu-button"
phx-click={JS.toggle(to: "#org-menu")}
class="flex items-center gap-2 text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
{if @current_organization, do: @current_organization.name, else: "Menu"}
<.icon name="hero-chevron-down" class="h-4 w-4" />
</button>
<div
id="org-menu"
class="hidden absolute top-full right-0 mt-2 w-48 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 z-50 dark:bg-zinc-800 dark:ring-zinc-700"
phx-click-away={JS.hide(to: "#org-menu")}
>
<div class="py-1">
<.link
:if={@current_organization}
navigate={~p"/orgs"}
class="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-700"
phx-click={JS.hide(to: "#org-menu")}
>
Switch Org
</.link>
<%= if @current_scope && @current_scope.user && @current_scope.user.is_superuser do %>
<.link
navigate={~p"/admin"}
class="flex items-center gap-2 px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-700"
phx-click={JS.hide(to: "#org-menu")}
>
<.icon name="hero-shield-check" class="w-4 h-4" /> Admin Panel
</.link>
<% end %>
<.link
navigate={~p"/users/settings"}
class="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-700"
phx-click={JS.hide(to: "#org-menu")}
>
Settings
</.link>
<.link
href={~p"/users/log-out"}
method="delete"
class="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
Log out
</.link>
</div>
</div>
</div>
</div>
</div>
</nav>
<main class="py-10">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{render_slot(@inner_block)}
</div>
</main>
</div>
<.flash_group flash={@flash} />
"""
end
attr :navigate, :string, required: true
attr :active, :boolean, default: false
slot :inner_block, required: true
defp nav_link(assigns) do
~H"""
<.link
navigate={@navigate}
class={[
"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium",
if(@active,
do: "border-zinc-900 text-zinc-900 dark:border-zinc-100 dark:text-zinc-100",
else:
"border-transparent text-zinc-700 hover:border-zinc-300 hover:text-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-700 dark:hover:text-zinc-100"
)
]}
>
{render_slot(@inner_block)}
</.link>
"""
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")}
phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
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!")}
phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
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
@doc """
Renders the admin layout.
Use this for admin pages that require superuser access.
## Examples
<Layouts.admin flash={@flash}>
<h1>Admin Content</h1>
</Layouts.admin>
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
slot :inner_block, required: true
def admin(assigns) do
~H"""
<main class="min-h-screen bg-zinc-50 dark:bg-zinc-950">
<nav class="bg-white border-b border-zinc-200 dark:border-zinc-800 dark:bg-zinc-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<.link
navigate={~p"/admin"}
class="flex items-center text-xl font-bold text-zinc-900 dark:text-zinc-100"
>
Towerops Admin
</.link>
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
<.link
navigate={~p"/admin/users"}
class="inline-flex items-center px-1 pt-1 text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
Users
</.link>
<.link
navigate={~p"/admin/organizations"}
class="inline-flex items-center px-1 pt-1 text-sm font-medium text-zinc-700 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
Organizations
</.link>
</div>
</div>
<div class="flex items-center">
<.link
navigate={~p"/orgs"}
class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
>
Back to App
</.link>
</div>
</div>
</div>
</nav>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{render_slot(@inner_block)}
</div>
</main>
<.flash_group flash={@flash} />
"""
end
end