8.5 KiB · text 5af55d9
defmodule GitGudWeb.Layouts do
@moduledoc """
Layout components for the app shell.
- `app/1` — the standard chrome (navbar + main column).
- `flash_group/1` — flash banners + LiveView connection-loss banners.
- `theme_toggle/1` — dropdown listing every theme in `GitGud.Themes`.
"""
use GitGudWeb, :html
embed_templates "layouts/*"
@doc false
# Client-side Sentry DSN (same as the server's). nil -> the browser
# SDK isn't loaded (dev/test). Safe to expose; the DSN is a public,
# write-only ingestion key.
def sentry_dsn, do: Application.get_env(:sentry, :dsn)
@doc """
Renders the app shell.
Pass `wide: true` for pages that should fill the viewport (file
browser, diffs); the default constrains the inner column.
"""
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 :wide, :boolean, default: false
slot :inner_block, required: true
def app(assigns) do
~H"""
<header class="navbar px-4 sm:px-6 lg:px-8 border-b border-base-300 bg-base-200">
<div class="flex-1">
<.link navigate={~p"/"} class="flex w-fit items-center gap-2" id="brand">
<span class="font-mono font-bold tracking-[0.16em] text-accent">GITGUD</span>
</.link>
</div>
<div class="flex-none">
<ul class="flex flex-row items-center gap-1">
<li>
<.link navigate={~p"/repositories"} class="btn btn-ghost btn-sm" id="nav-repos">
<.icon name="hero-folder-micro" class="size-4" />
<span class="hidden sm:inline">Repos</span>
</.link>
</li>
<%= if @current_scope && @current_scope.user do %>
<li>
<.link navigate={~p"/orgs"} class="btn btn-ghost btn-sm" id="nav-orgs">
<.icon name="hero-building-office-2-micro" class="size-4" />
<span class="hidden sm:inline">Orgs</span>
</.link>
</li>
<li :if={admin?(@current_scope)}>
<.link
navigate={~p"/admin/federation"}
class="btn btn-ghost btn-sm"
id="nav-admin"
>
<.icon name="hero-wrench-screwdriver-micro" class="size-4" />
<span class="hidden sm:inline">Admin</span>
</.link>
</li>
<li><span class="divider divider-horizontal mx-0" /></li>
<li>
<.theme_toggle />
</li>
<li>
<details class="dropdown dropdown-end">
<summary class="btn btn-ghost btn-sm">
<.icon name="hero-user-circle-micro" class="size-4" />
<span class="hidden sm:inline truncate max-w-[12ch]">
{user_label(@current_scope.user)}
</span>
</summary>
<ul class="dropdown-content menu bg-base-200 border border-base-300 rounded-box z-10 mt-1 w-56 p-1 shadow-lg">
<li class="menu-title font-mono text-xs truncate">
{@current_scope.user.email}
</li>
<li>
<.link navigate={~p"/repositories/new"}>
<.icon name="hero-plus-circle-micro" class="size-4" /> New repository
</.link>
</li>
<li>
<.link navigate={~p"/users/ci"}>
<.icon name="hero-rocket-launch-micro" class="size-4" /> My CI
</.link>
</li>
<li>
<.link navigate={~p"/users/settings"}>
<.icon name="hero-cog-6-tooth-micro" class="size-4" /> Settings
</.link>
</li>
<li>
<.link navigate={~p"/users/settings/ssh-keys"}>
<.icon name="hero-key-micro" class="size-4" /> SSH keys
</.link>
</li>
<li>
<.link navigate={~p"/users/settings/tokens"}>
<.icon name="hero-command-line-micro" class="size-4" /> API tokens
</.link>
</li>
<li>
<.link href={~p"/users/log-out"} method="delete">
<.icon name="hero-arrow-right-on-rectangle-micro" class="size-4" /> Log out
</.link>
</li>
</ul>
</details>
</li>
<% else %>
<li>
<.theme_toggle />
</li>
<li>
<.link navigate={~p"/users/log-in"} class="btn btn-ghost btn-sm">Log in</.link>
</li>
<li>
<.link navigate={~p"/users/register"} class="btn btn-primary btn-sm">
Register
</.link>
</li>
<% end %>
</ul>
</div>
</header>
<%= if @wide do %>
<main class="w-full">
{render_slot(@inner_block)}
</main>
<% else %>
<main class="px-4 py-8 sm:px-6 lg:px-8">
<div class="mx-auto max-w-5xl space-y-4">
{render_slot(@inner_block)}
</div>
</main>
<% end %>
<.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")}
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
defp user_label(%{email: email}) when is_binary(email),
do: hd(String.split(email, "@"))
defp user_label(_), do: "Account"
defp admin?(%{user: %{is_admin: true}}), do: true
defp admin?(_), do: false
@doc """
Theme picker dropdown listing every theme defined in `GitGud.Themes`.
Selection is persisted to localStorage by the script in
`root.html.heex`, which also handles the `phx:set-theme` event
dispatched here.
"""
def theme_toggle(assigns) do
assigns = assign_new(assigns, :themes, fn -> GitGud.Themes.all() end)
~H"""
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm gap-2" aria-label="Choose theme">
<.icon name="hero-swatch-micro" class="size-4" />
<span class="hidden sm:inline">Theme</span>
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-200 border border-base-300 rounded-box z-10 mt-1 w-44 p-1 shadow-lg"
>
<li>
<button
type="button"
phx-click={JS.dispatch("phx:set-theme")}
data-phx-theme="system"
class="flex items-center gap-2"
>
<.icon name="hero-computer-desktop-micro" class="size-4" /> System
</button>
</li>
<li :for={theme <- @themes}>
<button
type="button"
phx-click={JS.dispatch("phx:set-theme")}
data-phx-theme={theme}
class="flex items-center justify-between"
>
<span class="capitalize">{theme}</span>
<span
class="size-3 rounded-full border border-base-content/20"
data-theme={theme}
style="background: var(--color-primary)"
/>
</button>
</li>
</ul>
</div>
"""
end
end