370 lines
12 KiB
Elixir
370 lines
12 KiB
Elixir
defmodule WhoNeedHelpWeb.Layouts do
|
|
@moduledoc """
|
|
This module holds layouts and related functionality
|
|
used by your application.
|
|
"""
|
|
use WhoNeedHelpWeb, :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://phoenix.hexdocs.pm/scopes.html)"
|
|
|
|
slot :inner_block, required: true
|
|
|
|
def app(assigns) do
|
|
~H"""
|
|
<header class="sticky top-0 z-30 border-b border-base-300 bg-base-100 px-4 py-2 sm:px-6 lg:px-8">
|
|
<div class="mx-auto flex min-h-12 w-full max-w-7xl items-center justify-between gap-3">
|
|
<a
|
|
href="/"
|
|
class="flex min-w-0 items-center gap-3"
|
|
aria-label={gettext("Who Need Help home")}
|
|
>
|
|
<img
|
|
src={~p"/images/logo.svg"}
|
|
width="36"
|
|
height="36"
|
|
alt=""
|
|
class="size-9 shrink-0"
|
|
/>
|
|
<span class="truncate font-black tracking-tight">Who Need Help</span>
|
|
</a>
|
|
|
|
<nav class="min-w-0 flex-none" aria-label={gettext("Primary navigation")}>
|
|
<div class="hidden items-center gap-1 xl:flex">
|
|
<ul class="flex items-center gap-0.5">
|
|
<.primary_navigation_links current_scope={@current_scope} />
|
|
</ul>
|
|
|
|
<div
|
|
class="ml-1 flex items-center gap-1 border-l border-base-300 pl-2"
|
|
aria-label={gettext("Account and language")}
|
|
>
|
|
<.locale_switcher />
|
|
<.theme_toggle />
|
|
|
|
<%= if @current_scope do %>
|
|
<.link navigate={~p"/requests/new"} class="btn btn-primary btn-sm whitespace-nowrap">
|
|
{gettext("Ask for help")}
|
|
</.link>
|
|
<.account_menu current_scope={@current_scope} />
|
|
<% else %>
|
|
<.link navigate={~p"/users/register"} class="btn btn-ghost btn-sm">
|
|
{gettext("Register")}
|
|
</.link>
|
|
<.link navigate={~p"/users/log-in"} class="btn btn-primary btn-sm">
|
|
{gettext("Log in")}
|
|
</.link>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<details class="dropdown dropdown-end xl:hidden">
|
|
<summary
|
|
class="btn btn-ghost btn-square"
|
|
aria-label={gettext("Primary navigation")}
|
|
>
|
|
<.icon name="hero-bars-3" class="size-6" />
|
|
</summary>
|
|
|
|
<div class="dropdown-content mt-3 w-[min(20rem,calc(100vw-2rem))] overflow-hidden rounded-box border border-base-300 bg-base-100 shadow-xl">
|
|
<ul class="menu w-full p-2">
|
|
<.primary_navigation_links
|
|
current_scope={@current_scope}
|
|
link_class="justify-start"
|
|
/>
|
|
</ul>
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-base-300 px-4 py-3">
|
|
<.locale_switcher />
|
|
<.theme_toggle />
|
|
</div>
|
|
|
|
<%= if @current_scope do %>
|
|
<ul class="menu w-full border-t border-base-300 p-2">
|
|
<li>
|
|
<.link navigate={~p"/profile"}>{gettext("Profile")}</.link>
|
|
</li>
|
|
<li>
|
|
<.link href={~p"/users/settings"}>{gettext("Account settings")}</.link>
|
|
</li>
|
|
<li>
|
|
<.link href={~p"/users/log-out"} method="delete">{gettext("Log out")}</.link>
|
|
</li>
|
|
</ul>
|
|
<div class="border-t border-base-300 p-3">
|
|
<.link navigate={~p"/requests/new"} class="btn btn-primary btn-block">
|
|
{gettext("Ask for help")}
|
|
</.link>
|
|
</div>
|
|
<% else %>
|
|
<div class="grid grid-cols-2 gap-2 border-t border-base-300 p-3">
|
|
<.link navigate={~p"/users/register"} class="btn btn-outline">
|
|
{gettext("Register")}
|
|
</.link>
|
|
<.link navigate={~p"/users/log-in"} class="btn btn-primary">
|
|
{gettext("Log in")}
|
|
</.link>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</details>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="main-content" tabindex="-1" class="px-4 py-8 outline-none sm:px-6 lg:px-8">
|
|
<div class="mx-auto max-w-7xl space-y-4">
|
|
{render_slot(@inner_block)}
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="border-t border-base-300 px-4 py-6 text-center text-xs text-base-content/70">
|
|
<.link href={~p"/safety"} class="link">{gettext("Safety rules")}</.link>
|
|
<span class="mx-2">·</span>
|
|
<.link href={~p"/privacy"} class="link">{gettext("Privacy")}</.link>
|
|
<span class="mx-2">·</span>
|
|
<.link href={~p"/terms"} class="link">{gettext("Terms")}</.link>
|
|
<span class="mx-2">·</span>
|
|
<.link href={~p"/support"} class="link">{gettext("Support")}</.link>
|
|
<span class="mx-2">·</span>
|
|
<.link href={~p"/legal/content-removal"} class="link">{gettext("Report content")}</.link>
|
|
<span class="mx-2">·</span>
|
|
<.link href={~p"/feedback"} class="link">{gettext("Build Week feedback")}</.link>
|
|
</footer>
|
|
|
|
<.flash_group flash={@flash} />
|
|
"""
|
|
end
|
|
|
|
attr :current_scope, :map, default: nil
|
|
attr :link_class, :string, default: "btn btn-ghost btn-sm"
|
|
|
|
defp primary_navigation_links(assigns) do
|
|
~H"""
|
|
<li>
|
|
<.link navigate={~p"/requests"} class={@link_class}>
|
|
{gettext("Requests")}
|
|
</.link>
|
|
</li>
|
|
<li>
|
|
<.link navigate={~p"/activities"} class={@link_class}>
|
|
{gettext("Activities")}
|
|
</.link>
|
|
</li>
|
|
<li>
|
|
<.link navigate={~p"/categories/proposals"} class={@link_class}>
|
|
{gettext("Categories")}
|
|
</.link>
|
|
</li>
|
|
<li>
|
|
<.link navigate={~p"/leaderboard"} class={@link_class}>
|
|
{gettext("Helpers")}
|
|
</.link>
|
|
</li>
|
|
<li :if={@current_scope && @current_scope.user.role in [:moderator, :admin]}>
|
|
<.link navigate={~p"/moderation"} class={["btn-warning", @link_class]}>
|
|
{gettext("Moderation")}
|
|
</.link>
|
|
</li>
|
|
<li :if={@current_scope && @current_scope.user.role in [:moderator, :admin]}>
|
|
<.link navigate={~p"/support/operations"} class={["btn-warning", @link_class]}>
|
|
{gettext("Support queue")}
|
|
</.link>
|
|
</li>
|
|
"""
|
|
end
|
|
|
|
defp locale_switcher(assigns) do
|
|
~H"""
|
|
<div
|
|
class="join"
|
|
role="group"
|
|
aria-label={gettext("Account and language")}
|
|
>
|
|
<a
|
|
href="?locale=en"
|
|
class={[
|
|
"btn btn-ghost btn-xs join-item",
|
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "btn-active locale-active"
|
|
]}
|
|
aria-label="English"
|
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "page"}
|
|
>
|
|
EN
|
|
</a>
|
|
<a
|
|
href="?locale=uk"
|
|
class={[
|
|
"btn btn-ghost btn-xs join-item",
|
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "btn-active locale-active"
|
|
]}
|
|
aria-label="Українська"
|
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "page"}
|
|
>
|
|
UK
|
|
</a>
|
|
<a
|
|
href="?locale=ru"
|
|
class={[
|
|
"btn btn-ghost btn-xs join-item",
|
|
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "btn-active locale-active"
|
|
]}
|
|
aria-label="Русский"
|
|
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "page"}
|
|
>
|
|
RU
|
|
</a>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr :current_scope, :map, required: true
|
|
|
|
defp account_menu(assigns) do
|
|
~H"""
|
|
<details class="dropdown dropdown-end">
|
|
<summary class="btn btn-ghost btn-sm gap-1">
|
|
<.icon name="hero-user-circle" class="size-5" />
|
|
<span class="flex max-w-36 flex-col items-start overflow-hidden leading-tight">
|
|
<span class="max-w-full truncate">
|
|
{@current_scope.user.display_name || @current_scope.user.email}
|
|
</span>
|
|
<span
|
|
:if={@current_scope.user.display_name}
|
|
class="max-w-full truncate text-[0.65rem] font-normal opacity-60"
|
|
>
|
|
{@current_scope.user.email}
|
|
</span>
|
|
</span>
|
|
<.icon name="hero-chevron-down-micro" class="size-4 opacity-60" />
|
|
</summary>
|
|
<div class="dropdown-content mt-3 w-64 overflow-hidden rounded-box border border-base-300 bg-base-100 shadow-xl">
|
|
<ul class="menu w-full p-2">
|
|
<li>
|
|
<.link navigate={~p"/profile"}>{gettext("Profile")}</.link>
|
|
</li>
|
|
<li>
|
|
<.link href={~p"/users/settings"}>{gettext("Account settings")}</.link>
|
|
</li>
|
|
<li>
|
|
<.link href={~p"/users/log-out"} method="delete">{gettext("Log out")}</.link>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</details>
|
|
"""
|
|
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", to: ".phx-client-error #client-error")
|
|
}
|
|
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", to: ".phx-server-error #server-error")
|
|
}
|
|
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 [[data-theme-source=system]_&]:!left-0 transition-[left]" />
|
|
|
|
<button
|
|
class="flex p-2 cursor-pointer w-1/3"
|
|
phx-click={JS.dispatch("phx:set-theme")}
|
|
data-phx-theme="system"
|
|
aria-label={gettext("Use system theme")}
|
|
>
|
|
<.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"
|
|
aria-label={gettext("Use light theme")}
|
|
>
|
|
<.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"
|
|
aria-label={gettext("Use dark theme")}
|
|
>
|
|
<.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
|
|
</button>
|
|
</div>
|
|
"""
|
|
end
|
|
end
|