defmodule PhoenixKitWeb.Components.UserDashboardNav do @moduledoc """ User dashboard navigation components for the PhoenixKit user dashboard. Provides navigation elements specifically for user dashboard pages. """ use PhoenixKitWeb, :html alias PhoenixKit.Modules.Languages alias PhoenixKit.Modules.Languages.DialectMapper alias PhoenixKit.Settings alias PhoenixKit.Users.Auth.Scope alias PhoenixKit.Users.OAuthAvailability alias PhoenixKit.Utils.Routes alias PhoenixKitWeb.Components.Core.AdminLabel alias PhoenixKitWeb.Components.Core.LanguageSwitcher # Guest dropdown link catalog. Each entry is # `{key, path, icon, label_fn, setting_gate_fn}`; `label_fn`/`gate_fn` # are zero-arity so gettext + settings are evaluated at render time # (per-request locale / live setting), not at compile time. @guest_link_catalog [ {:login, "/users/log-in", "hero-arrow-right-on-rectangle"}, {:register, "/users/register", "hero-user-plus"}, {:reset, "/users/reset-password", "hero-key"}, {:magic_link, "/users/magic-link", "hero-sparkles"} ] @doc """ Renders the user widget for dashboard navigation. For authenticated visitors this is the avatar dropdown (email, the admin area, Settings, language switcher, log out). For anonymous visitors the same dropdown *shape* is rendered with a generic "not signed in" icon and guest-relevant links (log in, sign up, forgot password, magic link) plus the same language switcher — so a single widget covers both states and always offers a language switcher. ## One destination, two labels Every signed-in visitor gets exactly one entry leading to `/admin`, because `/admin` is the one page core declares unconditionally and admits EVERY authenticated visitor to (`PhoenixKitWeb.Users.Auth.landing_view?/1` exempts the index from the admin-area gate, and the page shows a permission-less visitor the welcome block and nothing else). An admin-area holder sees it as "Admin Panel" with a shield; everybody else sees "My Account" with a house. The two are mutually exclusive and share `admin_entry_label/1`. The URL is always built with `Routes.path("/admin")`, so a host running `config :phoenix_kit, admin_path: "/myaccount"` gets `/myaccount` here for free — `/admin` stays the canonical spelling in code. ## Attributes * `:scope` — current scope; `nil`/unauthenticated renders the guest dropdown. * `:current_path` — used for active-link highlighting and locale-switch URLs. Canonicalised before comparison, so a renamed admin segment still highlights. * `:current_locale` — the active locale. A full dialect (`"en-US"`) is accepted: every URL built here reduces it to the base code the router actually serves (`/en/…`), so passing `@current_locale` rather than `@current_locale_base` no longer emits a link that costs a redirect. * `:show_language_switcher` — include the in-menu language list (default `true`). Set `false` when the host renders a standalone switcher elsewhere to avoid a duplicate. Applies to both the signed-in and guest states. * `:guest_links` — which guest links may appear, e.g. `[:login, :register, :reset, :magic_link]` (default: all). Links are also gated by the `allow_registration` / `magic_link_login_enabled` settings, so this list can only narrow, never force-enable a disabled feature. * `:authenticated_links` — which authenticated-menu entries may appear, e.g. `[:admin, :dashboard, :settings, :logout]` (default: all). Same narrowing rule as `:guest_links` — `:admin` still requires `Scope.can_access_admin_area?/1` to be true, so listing it can't grant an entry a non-admin shouldn't see. Use this to hide entries a host app's own navigation already covers. `:dashboard` is the "My Account" half of the pair described above — the admin-area entry shown to a visitor `:admin` does not cover. It used to point at the deprecated user dashboard (`/dashboard`, `PhoenixKit.Install.Deprecations.user_dashboard_warning/0`), which a host can compile out with `user_dashboard_enabled: false` — leaving this menu offering a 404. The key name is kept so hosts passing an explicit list need no edit. """ attr(:scope, :any, default: nil) attr(:current_path, :string, default: "") attr(:current_locale, :string, default: "en") attr(:admin_edit_url, :string, default: nil) attr(:admin_edit_label, :string, default: nil) attr(:show_language_switcher, :boolean, default: true) attr(:guest_links, :list, default: [:login, :register, :reset, :magic_link]) attr(:authenticated_links, :list, default: [:admin, :dashboard, :settings, :logout]) def user_dropdown(assigns) do user = Scope.user(assigns.scope) multi_session_allowed? = assigns.scope && assigns.scope.multi_session_allowed? accounts = (assigns.scope && assigns.scope.multi_session_accounts) || [] assigns = assigns |> assign(:user, user) |> assign(:multi_session_allowed?, multi_session_allowed?) |> assign(:accounts, accounts) ~H""" <%= if @scope && PhoenixKit.Users.Auth.Scope.authenticated?(@scope) do %> <%= if @multi_session_allowed? do %> <% end %> <% else %> <.guest_dropdown current_path={@current_path} current_locale={@current_locale} guest_links={@guest_links} show_language_switcher={@show_language_switcher} /> <% end %> """ end # OAuth buttons for the "Add account" modal. # Uses the same provider availability checks as the main login page — # a provider button only appears when it is enabled in General Settings. # Each link targets /users/auth/:provider with add_account=1 so the OAuth # callback knows to append the result to the multi-session stack. # Duplicated from AdminNav's private component of the same name rather # than shared — keeps this frontend-facing module independent of the # admin-only one. attr :current_path, :string, default: "/" defp add_account_oauth_buttons(assigns) do google_enabled = OAuthAvailability.provider_enabled?(:google) github_enabled = OAuthAvailability.provider_enabled?(:github) facebook_enabled = OAuthAvailability.provider_enabled?(:facebook) any_enabled = google_enabled or github_enabled or facebook_enabled assigns = assigns |> assign(:google_enabled, google_enabled) |> assign(:github_enabled, github_enabled) |> assign(:facebook_enabled, facebook_enabled) |> assign(:any_enabled, any_enabled) ~H""" <%= if @any_enabled do %>
{gettext("Or add via")}
<%= if @google_enabled do %> <.link href={ Routes.path("/users/auth/google", locale: :none) <> "?add_account=1&return_to=#{URI.encode_www_form(@current_path)}" } class="btn btn-outline w-full flex items-center justify-center gap-2" > {gettext("Add Google account")} <% end %> <%= if @github_enabled do %> <.link href={ Routes.path("/users/auth/github", locale: :none) <> "?add_account=1&return_to=#{URI.encode_www_form(@current_path)}" } class="btn btn-outline w-full flex items-center justify-center gap-2" > {gettext("Add GitHub account")} <% end %> <%= if @facebook_enabled do %> <.link href={ Routes.path("/users/auth/facebook", locale: :none) <> "?add_account=1&return_to=#{URI.encode_www_form(@current_path)}" } class="btn btn-outline w-full flex items-center justify-center gap-2" > {gettext("Add Facebook account")} <% end %>
<% end %> """ end # Guest counterpart to the authenticated avatar dropdown: same shape and # styling, a generic "not signed in" trigger icon, guest-relevant links, # and the shared language switcher. attr(:current_path, :string, required: true) attr(:current_locale, :string, required: true) attr(:guest_links, :list, required: true) attr(:show_language_switcher, :boolean, required: true) defp guest_dropdown(assigns) do assigns = assign(assigns, :links, visible_guest_links(assigns.guest_links)) ~H""" """ end # Shared, independently scrollable language list used by both the # authenticated and guest dropdowns. Renders nothing when fewer than two # languages are enabled. Anchors are styled directly (no nested daisyUI # `