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 Phoenix.LiveView.JS alias PhoenixKit.Module.Languages alias PhoenixKit.ThemeConfig alias PhoenixKit.Utils.Routes import PhoenixKitWeb.Components.Core.Icon @doc """ Renders an admin navigation item with proper active state styling. ## Examples <.admin_nav_item href={Routes.locale_aware_path(assigns,"/admin/dashboard")} icon="dashboard" label="Dashboard" current_path={Routes.locale_aware_path(assigns,"/admin/dashboard")} /> <.admin_nav_item href={Routes.locale_aware_path(assigns,"/admin/users")} icon="users" label="Users" current_path={Routes.locale_aware_path(assigns,"/admin/dashboard")} 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 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 = assign(assigns, :active, active) ~H""" <.link navigate={@href} class={[ "flex items-center py-2 rounded-lg text-sm font-medium transition-colors", "hover:bg-base-200 group", if(@active, do: "bg-primary text-primary-content", else: "text-base-content hover:text-primary" ), 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" /> <% "entities" -> %> <.icon name="hero-cube" class="w-5 h-5" /> <% "language" -> %> <.icon name="hero-language" class="w-5 h-5" /> <% "document" -> %> <.icon name="hero-document-text" class="w-5 h-5" /> <% "maintenance" -> %> <.icon name="hero-wrench-screwdriver" class="w-5 h-5" /> <% _ -> %> <.icon name="hero-squares-2x2" class="w-5 h-5" /> <% end %>
""" end @doc """ Renders theme controller for admin panel. Based on EZNews theme system with DaisyUI integration. """ attr :mobile, :boolean, default: false def admin_theme_controller(assigns) do assigns = assigns |> assign(:dropdown_themes, ThemeConfig.dropdown_themes()) |> assign(:system_targets, ThemeConfig.slider_targets("system") |> Enum.join(",")) |> assign(:light_targets, ThemeConfig.slider_targets("light") |> Enum.join(",")) |> assign(:dark_targets, ThemeConfig.slider_targets("dark") |> Enum.join(",")) |> assign(:system_primary, ThemeConfig.slider_primary_theme("system")) |> assign(:light_primary, ThemeConfig.slider_primary_theme("light")) |> assign(:dark_primary, ThemeConfig.slider_primary_theme("dark")) |> assign(:system_primary, ThemeConfig.slider_primary_theme("system")) |> assign(:light_primary, ThemeConfig.slider_primary_theme("light")) |> assign(:dark_primary, ThemeConfig.slider_primary_theme("dark")) ~H"""
""" end @doc """ Renders language dropdown for top bar navigation. Shows globe icon with dropdown menu for language selection. """ attr :current_path, :string, default: "" attr :current_locale, :string, default: "en" def admin_language_dropdown(assigns) do # Only show if languages are enabled and there are enabled languages if Languages.enabled?() do enabled_languages = Languages.get_enabled_languages() if length(enabled_languages) > 1 do current_language = Enum.find(enabled_languages, &(&1["code"] == assigns.current_locale)) || %{"code" => assigns.current_locale, "name" => String.upcase(assigns.current_locale)} assigns = assigns |> assign(:enabled_languages, enabled_languages) |> assign(:current_language, current_language) ~H"""
""" else ~H"" end else ~H"" end 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" def admin_user_dropdown(assigns) do ~H""" <%= if @scope && PhoenixKit.Users.Auth.Scope.authenticated?(@scope) do %> <% else %> <%!-- Not Authenticated - Show Login Button --%> <.link href={Routes.locale_aware_path(assigns, "/users/log-in")} class="btn btn-primary btn-sm" > Login <% end %> """ end @doc """ Renders user information section for admin panel sidebar. Shows current user email and role information. """ attr :scope, :any, default: nil def admin_user_info(assigns) do ~H""" <%= if @scope && PhoenixKit.Users.Auth.Scope.authenticated?(@scope) do %>
{String.first(PhoenixKit.Users.Auth.Scope.user_email(@scope) || "?") |> String.upcase()}
{PhoenixKit.Users.Auth.Scope.user_email(@scope)}
<%= if PhoenixKit.Users.Auth.Scope.owner?(@scope) do %>
Owner
<% else %> <%= if PhoenixKit.Users.Auth.Scope.admin?(@scope) do %>
Admin
<% else %>
User
<% end %> <% end %>
<.link href={Routes.locale_aware_path(assigns, "/users/settings")} class="btn btn-ghost btn-xs flex-1" > <.link href={Routes.locale_aware_path(assigns, "/users/log-out")} method="delete" class="btn btn-ghost btn-xs flex-1 text-error hover:bg-error hover:text-error-content" >
<% else %>
Not authenticated
<.link href={Routes.locale_aware_path(assigns, "/users/log-in")} class="btn btn-primary btn-sm w-full" > Login
<% end %> """ end # Helper function to determine if navigation item is active defp nav_item_active?(current_path, href, nested) do current_parts = parse_admin_path(current_path) href_parts = parse_admin_path(href) exact_match?(current_parts, href_parts) or tab_match?(current_parts, href_parts) or parent_match?(current_parts, href_parts) or (!nested and hierarchical_match?(current_parts, href_parts)) end defp hierarchical_match?(current_parts, href_parts) do String.starts_with?(current_parts.base_path, href_parts.base_path <> "/") end # Check if paths match exactly defp exact_match?(current_parts, href_parts) do href_parts.base_path == current_parts.base_path && is_nil(href_parts.tab) && is_nil(current_parts.tab) end # Check if tab-specific paths match defp tab_match?(current_parts, href_parts) do href_parts.base_path == current_parts.base_path && href_parts.tab == current_parts.tab end # Check if parent page matches when on a tab defp parent_match?(current_parts, href_parts) do href_parts.base_path == current_parts.base_path && is_nil(href_parts.tab) && not is_nil(current_parts.tab) end # Helper function to parse admin path into components defp parse_admin_path(path) when is_binary(path) do # Remove query parameters and split path [path_part | _] = String.split(path, "?") # Get dynamic prefix and normalize paths prefix = PhoenixKit.Config.get_url_prefix() admin_prefix = if prefix == "/", do: "/admin", else: "#{prefix}/admin" base_path = path_part |> String.replace_prefix(prefix, "") |> strip_locale_segment() |> String.replace_prefix(admin_prefix, "") |> String.replace_prefix("/admin", "") |> case do # Default to dashboard for root "" -> "dashboard" "/" -> "dashboard" path -> String.trim_leading(path, "/") end # Extract tab parameter if present tab = if String.contains?(path, "?tab=") do path |> String.split("?tab=") |> List.last() |> String.split("&") |> List.first() else nil end %{base_path: base_path, tab: tab} end defp parse_admin_path(_), do: %{base_path: "dashboard", tab: nil} defp strip_locale_segment(path) do case String.split(path, "/", parts: 3) do ["", locale, rest] when rest != "" -> if locale_candidate?(locale) do "/" <> rest else path end _ -> path end end defp locale_candidate?(locale) do String.length(locale) in 2..5 and Regex.match?(~r/^[a-z]{2}(?:-[A-Za-z0-9]{2,})?$/, locale) end # Helper function to get language flag emoji defp get_language_flag(code) do case code do "en" -> "🇺🇸" "es" -> "🇪🇸" "fr" -> "🇫🇷" "de" -> "🇩🇪" "pt" -> "🇵🇹" "it" -> "🇮🇹" "nl" -> "🇳🇱" "ru" -> "🇷🇺" "zh-CN" -> "🇨🇳" "ja" -> "🇯🇵" _ -> "🌐" end end # Helper function to generate language switch URL defp generate_language_switch_url(current_path, new_locale) do # Get actual enabled language codes to properly detect locale prefixes enabled_language_codes = Languages.get_enabled_language_codes() # Remove PhoenixKit prefix if present normalized_path = String.replace_prefix(current_path || "", "/phoenix_kit", "") # Remove existing locale prefix only if it matches actual language codes clean_path = case String.split(normalized_path, "/", parts: 3) do ["", potential_locale, rest] -> if potential_locale in enabled_language_codes do "/" <> rest else normalized_path end _ -> normalized_path end # Build the new URL with the new locale prefix url_prefix = PhoenixKit.Config.get_url_prefix() base_prefix = if url_prefix == "/", do: "", else: url_prefix "#{base_prefix}/#{new_locale}#{clean_path}" end end