"""
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"""
<.theme_controller 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)
~H"""
<%= if @scope && PhoenixKit.Users.Auth.Scope.authenticated?(@scope) do %>
<%!-- User Avatar Button --%>
<%!-- Dropdown Menu --%>
<%!-- User Info Header --%>
{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 %>
<%!-- Settings Link --%>
<%= if PhoenixKit.Config.user_dashboard_enabled?() do %>
<% end %>
<%!-- Language Switcher (Admin Languages) --%>
<%= if @show_language_divider do %>
<% end %>
<%= if @show_language_section do %>
Language
<%!--
Independently scrollable language list — caps at ~5 visible rows.
Buttons are styled directly (no nested daisyUI
) so
the parent menu doesn't apply submenu indent (16px margin) or
li-child padding (12px) that would otherwise force horizontal scroll
and leave a weird left indent. `!p-0` resets the menu-child padding
the parent
applies to the wrapper
's children.
--%>
<%= for language <- @admin_languages do %>
<% end %>
<% end %>
<% 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
user = Scope.user(assigns.scope)
assigns =
assigns
|> assign(:user, user)
~H"""
<%= if @scope && PhoenixKit.Users.Auth.Scope.authenticated?(@scope) do %>
{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 %>
<% end %>
"""
end
# Helper function to determine if navigation item is active
defp nav_item_active?(current_path, href, _nested, exact_match_only) do
current_parts = parse_admin_path(current_path)
href_parts = parse_admin_path(href)
# Base matching: exact, tab, or parent match
base_matches =
exact_match?(current_parts, href_parts) or
tab_match?(current_parts, href_parts) or
parent_match?(current_parts, href_parts)
# For exact_match_only items (like Dashboard), skip hierarchical matching
# For all other items (including nested), use hierarchical matching
# This allows nested items like Products to be active on /products/:id/edit
if exact_match_only do
base_matches
else
base_matches or hierarchical_match?(current_parts, href_parts)
end
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
# Only treat exact empty paths as dashboard (for /admin)
"" -> ""
"/" -> ""
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
# Match both base language codes (en) and full dialect codes (en-US)
Regex.match?(~r/^[a-z]{2,3}(-[A-Za-z]{2,4})?$/i, locale)
end
# Helper function to get languages for admin nav display
# Uses the unified Languages module as the single source of truth.
# The final `dedupe_names/1` call drops the country qualifier from
# `:name` when only one dialect of a given base language is
# configured (e.g. "German (Germany)" → "German") — same rule the
# frontend dropdown applies; the helper lives in
# `Core.LanguageSwitcher` so all menus share one implementation.
defp get_admin_languages do
if Code.ensure_loaded?(Languages) do
Languages.get_display_languages()
|> Enum.filter(fn lang -> is_map(lang) and Map.get(lang, :is_enabled, false) end)
|> Enum.map(&enrich_language/1)
|> Enum.reject(&is_nil/1)
|> LanguageSwitcher.dedupe_names()
else
[]
end
end
defp enrich_language(lang) do
code = if is_struct(lang), do: lang.code, else: lang[:code]
if is_binary(code) do
case Languages.get_predefined_language(code) do
%{} = predefined -> predefined
_ -> %{code: code, name: Map.get(lang, :name, code), flag: "🌐", native: ""}
end
end
end
# Helper function to get language flag emoji
defp get_language_flag(code) when is_binary(code) do
if Code.ensure_loaded?(Languages) do
case Languages.get_predefined_language(code) do
%{flag: flag} -> flag
nil -> "🌐"
end
else
"🌐"
end
end
# Build URL with base code - expects base code directly (e.g., "en" not "en-US")
# Uses Routes.path/2 which automatically skips locale prefix for default language
defp build_locale_url(current_path, base_code) do
# Get valid language codes from the unified Languages module
language_codes =
if Code.ensure_loaded?(Languages),
do: Languages.enabled_locale_codes(),
else: []
base_codes = Enum.map(language_codes, &DialectMapper.extract_base/1)
valid_codes = Enum.uniq(language_codes ++ base_codes)
# Remove PhoenixKit prefix if present (use dynamic config, not hardcoded)
url_prefix = PhoenixKit.Config.get_url_prefix()
prefix_to_remove = if url_prefix == "/", do: "", else: url_prefix
normalized_path = String.replace_prefix(current_path || "", prefix_to_remove, "")
# 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 valid_codes, do: "/" <> rest, else: normalized_path
["", potential_locale] ->
if potential_locale in valid_codes, do: "/", else: normalized_path
_ ->
normalized_path
end
Routes.admin_path(clean_path, base_code)
end
# Legacy helper - kept for backward compatibility
defp generate_language_switch_url(current_path, new_locale) do
base_code = DialectMapper.extract_base(new_locale)
build_locale_url(current_path, base_code)
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.
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 %>
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"
>
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"
>
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"
>
Add Facebook account
<% end %>