Packages

phoenix_kit

1.7.145
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web components admin_nav.ex
Raw

lib/phoenix_kit_web/components/admin_nav.ex

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.Utils.Routes
alias PhoenixKitWeb.Components.Core.LanguageSwitcher
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 %>
<span class={[@icon, "w-4 h-4 mr-2 flex-shrink-0"]}></span>
<span class="text-sm truncate">{@label}</span>
<% else %>
<div class="w-4 h-4 mr-2 flex-shrink-0">
<.admin_nav_icon icon={@icon} active={@active} />
</div>
<span class="text-sm truncate">{@label}</span>
<% end %>
<% else %>
<.admin_nav_icon icon={@icon} active={@active} />
<span class="ml-3 font-medium truncate">{@label}</span>
<% end %>
</.link>
"""
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"""
<div class="flex items-center flex-shrink-0">
<%= 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" />
<% "seo" -> %>
<.icon name="hero-magnifying-glass-circle" 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" />
<% "maintenance" -> %>
<.icon name="hero-wrench-screwdriver" 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 %>
</div>
"""
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)
# 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(: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 %>
<div class="dropdown dropdown-end">
<%!-- User Avatar Button --%>
<div
tabindex="0"
role="button"
class="cursor-pointer hover:opacity-80 transition-opacity"
>
<PhoenixKitWeb.Components.Core.UserInfo.user_avatar
user={@user}
size="md"
class="!rounded-lg"
/>
</div>
<%!-- Dropdown Menu --%>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[60] w-64 p-2 shadow-xl border border-base-300 mt-3 max-h-[calc(100vh-5rem)] overflow-y-auto flex-nowrap"
>
<%!-- User Info Header --%>
<li class="menu-title px-4 py-2">
<div class="flex flex-col gap-1">
<span class="text-sm font-medium text-base-content truncate">
{PhoenixKit.Users.Auth.Scope.user_email(@scope)}
</span>
<div>
<%= if PhoenixKit.Users.Auth.Scope.owner?(@scope) do %>
<div class="badge badge-error badge-xs">Owner</div>
<% else %>
<%= if PhoenixKit.Users.Auth.Scope.admin?(@scope) do %>
<div class="badge badge-warning badge-xs">Admin</div>
<% else %>
<div class="badge badge-ghost badge-xs">User</div>
<% end %>
<% end %>
</div>
</div>
</li>
<%!-- Settings Link --%>
<%= if PhoenixKit.Config.user_dashboard_enabled?() do %>
<li>
<.link
href={Routes.locale_aware_path(assigns, "/dashboard/settings")}
class="flex items-center gap-3"
>
<PhoenixKitWeb.Components.Core.Icons.icon_settings class="w-4 h-4" />
<span>Settings</span>
</.link>
</li>
<% end %>
<%!-- Language Switcher (Admin Languages) --%>
<%= if @show_language_divider do %>
<div class="divider my-0"></div>
<% end %>
<%= if @show_language_section do %>
<li class="menu-title px-4 py-1">
<span class="text-xs">Language</span>
</li>
<%!--
Independently scrollable language list — caps at ~5 visible rows.
Buttons are styled directly (no nested daisyUI <ul class="menu">) 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 <ul> applies to the wrapper <li>'s children.
--%>
<li class="p-0 !bg-transparent hover:!bg-transparent focus:!bg-transparent">
<div class="!p-0 block w-full max-h-60 overflow-y-auto overflow-x-hidden !bg-transparent hover:!bg-transparent focus:!bg-transparent">
<%= for language <- @admin_languages do %>
<button
type="button"
phx-click="phoenix_kit_set_locale"
phx-value-locale={language.code}
phx-value-url={generate_language_switch_url(@current_path, language.code)}
class={[
"w-full flex items-center gap-3 rounded-lg px-4 py-2 text-sm",
"focus:outline-none focus-visible:outline-none",
"phx-click-loading:opacity-60 phx-click-loading:pointer-events-none",
if(language.code == @current_locale,
do:
"bg-primary !text-primary-content hover:bg-primary hover:!text-primary-content active:!bg-primary/80",
else:
"!text-base-content hover:bg-base-200 hover:!text-base-content focus:!text-base-content active:!text-base-content active:!bg-base-300"
)
]}
>
<span class="text-lg">{get_language_flag(language.code)}</span>
<span>{language.name}</span>
<%= if language.code == @current_locale do %>
<PhoenixKitWeb.Components.Core.Icons.icon_check class="w-4 h-4 ml-auto" />
<% end %>
</button>
<% end %>
</div>
</li>
<% end %>
<%= if @multi_session_allowed? do %>
<div class="divider my-0"></div>
<li class="menu-title px-4 py-1">
<span class="text-xs">Accounts</span>
</li>
<%= for account <- @accounts do %>
<li class="p-0">
<%= if account.active? do %>
<div class="flex items-center gap-3 px-4 py-2 rounded-lg bg-base-200">
<span class="flex-1 min-w-0 break-words">{account.email}</span>
<span class="badge badge-xs badge-ghost">{account.role}</span>
<PhoenixKitWeb.Components.Core.Icons.icon_check class="w-4 h-4 ml-auto" />
</div>
<% else %>
<div class="flex items-center gap-2 px-1">
<.form
for={%{}}
action={Routes.locale_aware_path(assigns, "/users/session/active")}
method="put"
class="flex-1"
>
<input type="hidden" name="ref" value={account.ref} />
<input type="hidden" name="return_to" value={@current_path} />
<button
type="submit"
class="flex w-full items-center gap-3 px-3 py-2 rounded-lg hover:bg-base-200"
>
<span class="flex-1 min-w-0 break-words">{account.email}</span>
<span class="badge badge-xs badge-ghost ml-auto">{account.role}</span>
</button>
</.form>
<%= unless account.root? do %>
<.form
for={%{}}
action={
Routes.locale_aware_path(assigns, "/users/session/accounts/#{account.ref}")
}
method="delete"
>
<input type="hidden" name="return_to" value={@current_path} />
<button
type="submit"
class="btn btn-ghost btn-xs btn-square text-error"
title="Remove"
>
</button>
</.form>
<% end %>
</div>
<% end %>
</li>
<% end %>
<li class="p-0">
<label
for="pk-add-account-modal"
class="flex items-center gap-3 px-4 py-2 rounded-lg hover:bg-base-200 cursor-pointer"
>
<PhoenixKitWeb.Components.Core.Icons.icon_settings class="w-4 h-4" />
<span>Add account</span>
</label>
</li>
<% end %>
<div class="divider my-0"></div>
<%!-- Log Out Link --%>
<li>
<.link
href={Routes.locale_aware_path(assigns, "/users/log-out")}
method="delete"
class="flex items-center gap-3 text-error hover:bg-error hover:text-error-content"
>
<PhoenixKitWeb.Components.Core.Icons.icon_logout class="w-4 h-4" />
<span>Log out</span>
</.link>
</li>
<%= if @multi_session_allowed? and length(@accounts) > 1 do %>
<li>
<.link
href={Routes.locale_aware_path(assigns, "/users/log-out") <> "?all=1"}
method="delete"
class="flex items-center gap-3 text-error hover:bg-error hover:text-error-content"
>
<PhoenixKitWeb.Components.Core.Icons.icon_logout class="w-4 h-4" />
<span>Log out from all accounts</span>
</.link>
</li>
<% end %>
</ul>
</div>
<%= if @multi_session_allowed? do %>
<input type="checkbox" id="pk-add-account-modal" class="modal-toggle" />
<div class="modal" role="dialog">
<div class="modal-box">
<h3 class="text-lg font-bold mb-4">Add account</h3>
<.form
for={%{}}
action={Routes.locale_aware_path(assigns, "/users/session/accounts")}
method="post"
class="space-y-4"
>
<input type="hidden" name="return_to" value={@current_path} />
<div class="form-control">
<label class="label"><span class="label-text">Email or username</span></label>
<input
name="user[email_or_username]"
type="text"
required
class="input input-bordered w-full"
/>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Password</span></label>
<input
name="user[password]"
type="password"
required
class="input input-bordered w-full"
/>
</div>
<div class="modal-action">
<label for="pk-add-account-modal" class="btn btn-outline">Cancel</label>
<button type="submit" class="btn btn-primary">Add account</button>
</div>
</.form>
<.add_account_oauth_buttons current_path={@current_path} />
</div>
<label class="modal-backdrop" for="pk-add-account-modal">Close</label>
</div>
<% end %>
<% else %>
<%!-- Not Authenticated - Show Login Button --%>
<.link
href={Routes.locale_aware_path(assigns, "/users/log-in")}
class="btn btn-primary btn-sm"
>
Login
</.link>
<% 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 %>
<div class="bg-base-200 rounded-lg p-3 text-sm">
<div class="flex items-center gap-2 mb-2">
<PhoenixKitWeb.Components.Core.UserInfo.user_avatar
user={@user}
size="sm"
class="!rounded-md"
/>
<div class="flex-1 min-w-0">
<div class="truncate text-xs font-medium text-base-content">
{PhoenixKit.Users.Auth.Scope.user_email(@scope)}
</div>
<%= if PhoenixKit.Users.Auth.Scope.owner?(@scope) do %>
<div class="badge badge-error badge-xs">Owner</div>
<% else %>
<%= if PhoenixKit.Users.Auth.Scope.admin?(@scope) do %>
<div class="badge badge-warning badge-xs">Admin</div>
<% else %>
<div class="badge badge-ghost badge-xs">User</div>
<% end %>
<% end %>
</div>
</div>
<div class="flex gap-1">
<.link
href={Routes.locale_aware_path(assigns, "/dashboard/settings")}
class="btn btn-ghost btn-xs flex-1"
>
<PhoenixKitWeb.Components.Core.Icons.icon_settings class="w-3 h-3" />
</.link>
<.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"
>
<PhoenixKitWeb.Components.Core.Icons.icon_logout />
</.link>
</div>
</div>
<% else %>
<div class="bg-base-200 rounded-lg p-3 text-center text-sm">
<div class="text-base-content/70 mb-2">Not authenticated</div>
<.link
href={Routes.locale_aware_path(assigns, "/users/log-in")}
class="btn btn-primary btn-sm w-full"
>
Login
</.link>
</div>
<% 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)
apple_enabled = OAuthAvailability.provider_enabled?(:apple)
github_enabled = OAuthAvailability.provider_enabled?(:github)
facebook_enabled = OAuthAvailability.provider_enabled?(:facebook)
any_enabled = google_enabled or apple_enabled or github_enabled or facebook_enabled
assigns =
assigns
|> assign(:google_enabled, google_enabled)
|> assign(:apple_enabled, apple_enabled)
|> assign(:github_enabled, github_enabled)
|> assign(:facebook_enabled, facebook_enabled)
|> assign(:any_enabled, any_enabled)
~H"""
<%= if @any_enabled do %>
<div class="mt-4">
<div class="divider text-base-content/60 text-xs">Or add via</div>
<div class="space-y-2">
<%= 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"
>
<PhoenixKitWeb.Components.Core.Icons.icon_google class="w-5 h-5" />
<span>Add Google account</span>
</.link>
<% end %>
<%= if @apple_enabled do %>
<.link
href={
Routes.path("/users/auth/apple", 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"
>
<PhoenixKitWeb.Components.Core.Icons.icon_apple class="w-5 h-5" />
<span>Add Apple account</span>
</.link>
<% 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"
>
<PhoenixKitWeb.Components.Core.Icons.icon_github class="w-5 h-5" />
<span>Add GitHub account</span>
</.link>
<% 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"
>
<PhoenixKitWeb.Components.Core.Icons.icon_facebook class="w-5 h-5" />
<span>Add Facebook account</span>
</.link>
<% end %>
</div>
</div>
<% end %>
"""
end
end