"""
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"""
<.icon name="hero-swatch" class="w-5 h-5" />
<%= for theme <- @dropdown_themes do %>
<% end %>
"""
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
# Get admin languages from settings (separate from the language module)
admin_languages = get_admin_languages()
# Extract base code from current locale for matching
current_base = DialectMapper.extract_base(assigns.current_locale)
# Transform languages: code = base (for URLs), dialect = full (for preferences)
transformed_languages =
Enum.map(admin_languages, fn lang ->
dialect = lang["code"]
base = DialectMapper.extract_base(dialect)
lang
|> Map.put("code", base)
|> Map.put("dialect", dialect)
end)
current_language =
Enum.find(transformed_languages, &(&1["code"] == current_base)) ||
%{
"code" => current_base,
"dialect" => assigns.current_locale,
"name" => String.upcase(current_base)
}
# Hide dropdown when only 1 language is configured
show_dropdown = length(transformed_languages) > 1
assigns =
assigns
|> assign(:enabled_languages, transformed_languages)
|> assign(:current_language, current_language)
|> assign(:current_base, current_base)
|> assign(:show_dropdown, show_dropdown)
~H"""
"""
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
user = Scope.user(assigns.scope)
avatar_file_id = user && user.custom_fields && user.custom_fields["avatar_file_id"]
# Get admin languages info for the dropdown
admin_languages = get_admin_languages()
show_language_section = not Enum.empty?(admin_languages)
show_language_divider = PhoenixKit.Config.user_dashboard_enabled?() and show_language_section
assigns =
assigns
|> assign(:avatar_file_id, avatar_file_id)
|> 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 --%>
<%= if @avatar_file_id do %>
<% avatar_url = PhoenixKit.Modules.Storage.URLSigner.signed_url(@avatar_file_id, "medium") %>
<% 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)
avatar_file_id = user && user.custom_fields && user.custom_fields["avatar_file_id"]
assigns =
assigns
|> assign(:avatar_file_id, avatar_file_id)
|> assign(:user, user)
~H"""
<%= if @scope && PhoenixKit.Users.Auth.Scope.authenticated?(@scope) do %>
<%= if @avatar_file_id do %>
<% avatar_url =
PhoenixKit.Modules.Storage.URLSigner.signed_url(@avatar_file_id, "small") %>
<% 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)
# For nested items, use only exact and tab matching to prevent parent highlighting
if nested do
exact_match?(current_parts, href_parts) or tab_match?(current_parts, href_parts)
else
# For top-level items with exact_match_only, skip hierarchical matching
base_matches =
exact_match?(current_parts, href_parts) or
tab_match?(current_parts, href_parts) or
parent_match?(current_parts, href_parts)
if exact_match_only do
base_matches
else
base_matches or hierarchical_match?(current_parts, href_parts)
end
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
# Only match base language codes (2 letters, no hyphen)
# Full dialect codes (en-US) are no longer used in URLs
String.length(locale) == 2 and Regex.match?(~r/^[a-z]{2}$/i, locale)
end
# Helper function to get admin languages from settings
# Returns empty list if Languages module is disabled
defp get_admin_languages do
# If Languages module is not enabled, return empty list to hide dropdown
if Languages.enabled?() do
# Read admin languages from settings cache (not all enabled languages)
# Note: admin_languages is stored as JSON string in value column
admin_languages_json =
Settings.get_setting_cached("admin_languages", nil) ||
Jason.encode!(["en-US"])
admin_language_codes =
case Jason.decode(admin_languages_json) do
{:ok, codes} when is_list(codes) -> codes
_ -> ["en-US"]
end
# Map codes to full language objects
admin_language_codes
|> Enum.map(fn code ->
case Languages.get_predefined_language(code) do
%{name: name, flag: flag, native: native} ->
%{
"code" => code,
"name" => name,
"flag" => flag,
"native" => native
}
nil ->
# Fallback for unknown codes
%{
"code" => code,
"name" => code,
"flag" => "🌐",
"native" => ""
}
end
end)
else
# Module disabled, return empty list
[]
end
end
# Helper function to get language flag emoji
defp get_language_flag(code) when is_binary(code) do
case Languages.get_predefined_language(code) do
%{flag: flag} -> flag
nil -> "🌐"
end
end
# Build URL with base code - expects base code directly (e.g., "en" not "en-US")
# Used by admin language dropdown where language["code"] is already the base code
# 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 both admin and frontend systems
admin_languages = get_admin_languages()
admin_language_codes = Enum.map(admin_languages, & &1["code"])
admin_base_codes = Enum.map(admin_language_codes, &DialectMapper.extract_base/1)
# Get frontend language codes from the Language Module
frontend_language_codes = Languages.enabled_locale_codes()
frontend_base_codes = Enum.map(frontend_language_codes, &DialectMapper.extract_base/1)
# Accept language if it's valid in EITHER admin or frontend (both full and base codes)
valid_language_codes =
(admin_language_codes ++ frontend_language_codes ++ admin_base_codes ++ frontend_base_codes)
|> Enum.uniq()
# 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_language_codes do
"/" <> rest
else
normalized_path
end
["", potential_locale] ->
if potential_locale in valid_language_codes do
"/"
else
normalized_path
end
_ ->
normalized_path
end
# Use Routes.path which handles default language logic
# Default language gets clean URLs (no prefix), other languages get prefixed
Routes.path(clean_path, locale: 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
end