defmodule PhoenixKitWeb.Components.AdminNav do
@moduledoc """
Admin navigation components for the PhoenixKit admin panel.
Provides consistent navigation elements for both desktop sidebar and mobile drawer.
"""
use Phoenix.Component
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Languages.DialectMapper
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Users.OAuthAvailability
alias PhoenixKit.Users.Role
alias PhoenixKit.Utils.Routes
alias PhoenixKitWeb.Components.Core.LanguageSwitcher
alias PhoenixKitWeb.Users.MultiSession
import PhoenixKitWeb.Components.Core.Icon
import PhoenixKitWeb.Components.Core.ThemeController, only: [theme_controller: 1]
@doc """
Renders an admin navigation item with proper active state styling.
## Examples
<.admin_nav_item
href={Routes.locale_aware_path(assigns,"/admin")}
icon="dashboard"
label="Dashboard"
current_path={Routes.locale_aware_path(assigns,"/admin")}
/>
<.admin_nav_item
href={Routes.locale_aware_path(assigns,"/admin/users")}
icon="users"
label="Users"
current_path={Routes.locale_aware_path(assigns,"/admin")}
mobile={true}
/>
"""
attr(:href, :string, required: true)
attr(:icon, :string, required: true)
attr(:label, :string, required: true)
attr(:description, :string, default: nil)
attr(:current_path, :string, required: true)
attr(:mobile, :boolean, default: false)
attr(:nested, :boolean, default: false)
attr(:disable_active, :boolean, default: false)
attr(:exact_match_only, :boolean, default: false)
attr(:submenu_open, :boolean, default: false)
def admin_nav_item(assigns) do
active =
if assigns.disable_active,
do: false,
else:
nav_item_active?(
assigns.current_path,
assigns.href,
assigns.nested,
assigns.exact_match_only
)
assigns = assign(assigns, :active, active)
~H"""
<.link
navigate={@href}
class={[
"flex items-center py-2 rounded-lg text-sm font-medium transition-colors group",
cond do
@active ->
"bg-primary text-primary-content hover:bg-primary/90"
@submenu_open ->
"bg-base-200/50 text-base-content hover:bg-base-200 hover:text-primary"
true ->
"text-base-content hover:bg-base-200 hover:text-primary"
end,
if(@mobile, do: "w-full", else: ""),
if(@nested, do: "pl-8 pr-3", else: "px-3")
]}
>
<%= if @nested do %>
<%!-- Nested item with custom hero icon --%>
<%= if String.starts_with?(@icon, "hero-") do %>
{@label}
<% else %>
<.admin_nav_icon icon={@icon} active={@active} />
{@label}
<% end %>
<% else %>
<.admin_nav_icon icon={@icon} active={@active} />
{@label}
<% end %>
"""
end
@doc """
Renders an icon for admin navigation items.
"""
attr(:icon, :string, required: true)
attr(:active, :boolean, default: false)
def admin_nav_icon(assigns) do
~H"""
<%= case @icon do %>
<% "dashboard" -> %>
<.icon name="hero-home" class="w-5 h-5" />
<% "users" -> %>
<.icon name="hero-users" class="w-5 h-5" />
<% "roles" -> %>
<.icon name="hero-shield-check" class="w-5 h-5" />
<% "modules" -> %>
<.icon name="hero-puzzle-piece" class="w-5 h-5" />
<% "settings" -> %>
<.icon name="hero-cog-6-tooth" class="w-5 h-5" />
<% "sessions" -> %>
<.icon name="hero-computer-desktop" class="w-5 h-5" />
<% "live_sessions" -> %>
<.icon name="hero-eye" class="w-5 h-5" />
<% "referral_codes" -> %>
<.icon name="hero-ticket" class="w-5 h-5" />
<% "email" -> %>
<.icon name="hero-envelope" class="w-5 h-5" />
<% "billing" -> %>
<.icon name="hero-banknotes" class="w-5 h-5" />
<% "entities" -> %>
<.icon name="hero-cube" class="w-5 h-5" />
<% "ticket" -> %>
<.icon name="hero-chat-bubble-left-right" class="w-5 h-5" />
<% "ai" -> %>
<.icon name="hero-cpu-chip" class="w-5 h-5" />
<% "language" -> %>
<.icon name="hero-language" class="w-5 h-5" />
<% "crawlers" -> %>
<.icon name="hero-bug-ant" class="w-5 h-5" />
<% "sitemap" -> %>
<.icon name="hero-map" class="w-5 h-5" />
<% "document" -> %>
<.icon name="hero-document-text" class="w-5 h-5" />
<% "legal" -> %>
<.icon name="hero-scale" class="w-5 h-5" />
<% "organization" -> %>
<.icon name="hero-building-office" class="w-5 h-5" />
<% "storage" -> %>
<.icon name="hero-folder" class="w-5 h-5" />
<% "photo" -> %>
<.icon name="hero-photo" class="w-5 h-5" />
<% "jobs" -> %>
<.icon name="hero-queue-list" class="w-5 h-5" />
<% "shop" -> %>
<.icon name="hero-shopping-bag" class="w-5 h-5" />
<% _ -> %>
<.icon name="hero-squares-2x2" class="w-5 h-5" />
<% end %>
"""
end
@doc """
Renders theme controller for admin panel.
Uses the shared theme_controller component with all themes.
"""
attr(:mobile, :boolean, default: false)
def admin_theme_controller(assigns) do
~H"""
<%!-- :dashboard_themes applies HERE too — hardcoding :all meant a host's
narrowed theme list governed only the user dashboard while every
admin page kept the full catalogue, silently. --%>
<.theme_controller
themes={PhoenixKit.Config.get(:dashboard_themes, :all)}
id="admin-theme-dropdown"
/>
"""
end
@doc """
Renders user dropdown for top bar navigation.
Shows user avatar with dropdown menu containing email, role, settings and logout.
"""
attr(:scope, :any, default: nil)
attr(:current_path, :string, default: "")
attr(:current_locale, :string, default: "en")
attr(:accounts, :list, default: [])
attr(:multi_session_allowed?, :boolean, default: false)
def admin_user_dropdown(assigns) do
user = Scope.user(assigns.scope)
# Highlight by BASE code, not full-dialect equality. `@current_locale`
# may be a base ("en") or a resolved dialect that differs from the
# enabled one (e.g. `resolve_dialect("en")` => "en-US" while English is
# enabled as plain "en"), so a raw `==` against the enabled code never
# matched on locales with a regional default. Same rule as
# UserDashboardNav.language_menu_section/1.
current_base = DialectMapper.extract_base(assigns.current_locale)
# Get admin languages info for the dropdown
admin_languages =
Enum.map(get_admin_languages(), fn language ->
Map.put(language, :active?, DialectMapper.extract_base(language.code) == current_base)
end)
show_language_section = not Enum.empty?(admin_languages)
show_language_divider = PhoenixKit.Config.user_dashboard_enabled?() and show_language_section
assigns =
assigns
|> assign(:user, user)
|> assign(:admin_languages, admin_languages)
|> assign(:show_language_section, show_language_section)
|> assign(:show_language_divider, show_language_divider)
|> assign(:accounts_removable?, any_removable_account?(assigns.accounts))
~H"""
<%= if @scope && PhoenixKit.Users.Auth.Scope.authenticated?(@scope) do %>
<%!-- User Avatar Button --%>
<%!-- Dropdown Menu --%>
<%!-- User Info Header --%>
<%!-- Settings Link. Not gated on `user_dashboard_enabled?` any --%>
<%!-- more: the account page it points at is routed --%>
<%!-- unconditionally, and the old guard meant a host that --%>
<%!-- compiled the dashboard out left admins with no way in to --%>
<%!-- their own email and password. --%>
-
<.link
href={Routes.locale_aware_user_settings_path(assigns)}
class="flex items-center gap-3"
>
Settings
<%!-- Language Switcher (Admin Languages) --%>
<%= if @show_language_divider do %>
<% end %>
<%= if @show_language_section do %>
<%!--
Independently scrollable language list — caps at ~5 visible rows.
Buttons are styled directly (no nested daisyUI