Packages

phoenix_kit

1.7.2
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 users settings.html.heex
Raw

lib/phoenix_kit_web/users/settings.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Account Settings"
>
<div class="flex items-center justify-center py-8 min-h-[80vh]">
<div class="w-full max-w-lg px-4">
<div class="text-center mb-8">
<div class="flex items-center justify-center mb-4">
<PhoenixKitWeb.Components.Core.Icons.icon_settings class="w-12 h-12 text-primary" />
</div>
<h1 class="text-3xl font-bold mb-2">Account Settings</h1>
<p class="text-base-content/60 text-sm">
Manage your account email address and password settings
</p>
</div>
<div class="space-y-8">
<%!-- Email Settings Card --%>
<div class="card bg-base-100 shadow-2xl">
<div class="card-body">
<div class="flex items-center mb-4">
<PhoenixKitWeb.Components.Core.Icons.icon_email class="w-6 h-6 text-primary mr-3" />
<div>
<h2 class="text-xl font-bold">Email Address</h2>
<p class="text-sm text-base-content/60">Change your account email address</p>
</div>
</div>
<.simple_form
for={@email_form}
id="email_form"
phx-submit="update_email"
phx-change="validate_email"
>
<.input
field={@email_form[:email]}
type="email"
label="Email"
required
>
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_email class="w-4 h-4 mr-2" />
</:icon>
</.input>
<.input
field={@email_form[:current_password]}
name="current_password"
id="current_password_for_email"
type="password"
label="Current password"
value={@email_form_current_password}
required
>
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_lock class="w-4 h-4 mr-2" />
</:icon>
</.input>
<:actions>
<.button
phx-disable-with="Changing..."
class="btn-primary transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<PhoenixKitWeb.Components.Core.Icons.icon_email class="w-5 h-5 mr-2" />
Change Email
</.button>
</:actions>
</.simple_form>
</div>
</div>
<%!-- Profile Settings Card --%>
<div class="card bg-base-100 shadow-2xl">
<div class="card-body">
<div class="flex items-center mb-4">
<PhoenixKitWeb.Components.Core.Icons.icon_user_profile class="w-6 h-6 text-primary mr-3" />
<div>
<h2 class="text-xl font-bold">Profile Information</h2>
<p class="text-sm text-base-content/60">
Update your personal information and timezone preference
</p>
</div>
</div>
<%!-- Avatar Upload Section --%>
<div class="mb-6">
<label class="label">
<span class="label-text font-semibold">Profile Picture</span>
</label>
<div class="flex items-start gap-6">
<%!-- Avatar Preview --%>
<div class="flex-shrink-0">
<%= if get_in(@phoenix_kit_current_user.custom_fields, ["avatar_file_id"]) do %>
<% avatar_url =
PhoenixKit.Modules.Storage.URLSigner.signed_url(
get_in(@phoenix_kit_current_user.custom_fields, ["avatar_file_id"]),
"thumbnail"
) %>
<img
src={avatar_url}
alt="Avatar"
class="w-32 h-32 rounded-full object-cover border-2 border-base-300"
/>
<% else %>
<div class="w-32 h-32 rounded-full bg-primary/10 border-2 border-base-300 flex items-center justify-center">
<span class="text-4xl font-bold text-primary">
{String.upcase(String.at(@phoenix_kit_current_user.email, 0))}
</span>
</div>
<% end %>
</div>
<%!-- Upload Controls --%>
<div class="flex-1">
<.file_upload
upload={@uploads.avatar}
variant="button"
label="Choose Profile Picture"
/>
<p class="text-sm text-base-content/60 mb-2">
Upload a profile picture (max 10MB). Accepts JPG, PNG, GIF.
</p>
<%!-- Success Message --%>
<%= if @last_uploaded_avatar_id do %>
<div class="alert alert-success text-sm">
<PhoenixKitWeb.Components.Core.Icons.icon_check class="stroke-current shrink-0 h-5 w-5" />
<span>Avatar uploaded successfully!</span>
</div>
<% end %>
</div>
</div>
<%!-- Divider after avatar section --%>
<div class="divider"></div>
</div>
<.simple_form
for={@profile_form}
id="profile_form"
phx-submit="update_profile"
phx-change="validate_profile"
>
<.input
field={@profile_form[:username]}
type="text"
label="Username"
>
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_user_profile class="w-4 h-4 mr-2" />
</:icon>
</.input>
<.input
field={@profile_form[:first_name]}
type="text"
label="First Name"
>
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_user_profile class="w-4 h-4 mr-2" />
</:icon>
</.input>
<.input field={@profile_form[:last_name]} type="text" label="Last Name">
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_user_profile class="w-4 h-4 mr-2" />
</:icon>
</.input>
<%!-- Custom Fields Section --%>
<%= if length(@custom_field_definitions) > 0 do %>
<%!-- Divider --%>
<div class="divider text-sm text-base-content/60">Additional Information</div>
<%= for field_def <- @custom_field_definitions do %>
<%= case field_def["type"] do %>
<% "text" -> %>
<.input
name={"custom_fields[#{field_def["key"]}]"}
type="text"
label={field_def["label"]}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% "textarea" -> %>
<.textarea
name={"custom_fields[#{field_def["key"]}]"}
label={field_def["label"]}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% "number" -> %>
<.input
name={"custom_fields[#{field_def["key"]}]"}
type="number"
label={field_def["label"]}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% "email" -> %>
<.input
name={"custom_fields[#{field_def["key"]}]"}
type="email"
label={field_def["label"]}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% "url" -> %>
<.input
name={"custom_fields[#{field_def["key"]}]"}
type="url"
label={field_def["label"]}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% "date" -> %>
<.input
name={"custom_fields[#{field_def["key"]}]"}
type="date"
label={field_def["label"]}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% "boolean" -> %>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name={"custom_fields[#{field_def["key"]}]"}
class="checkbox checkbox-primary"
value="true"
checked={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ==
true ||
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ==
"true"
}
/>
<span class="label-text">{field_def["label"]}</span>
</label>
</div>
<% "select" -> %>
<.select
name={"custom_fields[#{field_def["key"]}]"}
label={field_def["label"]}
options={Enum.map(field_def["options"], &{&1, &1})}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% "radio" -> %>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">{field_def["label"]}</span>
</label>
<div class="space-y-2">
<%= for option <- field_def["options"] do %>
<label class="label cursor-pointer justify-start gap-3">
<input
type="radio"
name={"custom_fields[#{field_def["key"]}]"}
class="radio radio-primary"
value={option}
checked={
get_in(@phoenix_kit_current_user.custom_fields, [
field_def["key"]
]) == option
}
/>
<span class="label-text">{option}</span>
</label>
<% end %>
</div>
</div>
<% "checkbox" -> %>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">{field_def["label"]}</span>
</label>
<div class="space-y-2">
<%= for option <- field_def["options"] do %>
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name={"custom_fields[#{field_def["key"]}][]"}
class="checkbox checkbox-primary"
value={option}
checked={
is_list(
get_in(@phoenix_kit_current_user.custom_fields, [
field_def["key"]
])
) &&
option in get_in(@phoenix_kit_current_user.custom_fields, [
field_def["key"]
])
}
/>
<span class="label-text">{option}</span>
</label>
<% end %>
</div>
</div>
<% _ -> %>
<%!-- Fallback for unknown field types --%>
<.input
name={"custom_fields[#{field_def["key"]}]"}
type="text"
label={field_def["label"]}
value={
get_in(@phoenix_kit_current_user.custom_fields, [field_def["key"]]) ||
field_def["default"]
}
required={field_def["required"]}
/>
<% end %>
<% end %>
<% end %>
<div id="timezone-detector">
<.select
field={@profile_form[:user_timezone]}
label="Personal Timezone"
options={@timezone_options}
/>
<%!-- Timezone Mismatch Warning --%>
<%= if assigns[:timezone_mismatch_warning] do %>
<div class="alert alert-warning text-sm mt-2">
<PhoenixKitWeb.Components.Core.Icons.icon_warning class="stroke-current shrink-0 h-5 w-5" />
<div>
<div class="font-semibold">Timezone Mismatch Detected</div>
<div class="text-xs">
{@timezone_mismatch_warning}
</div>
</div>
</div>
<% end %>
<%!-- Browser Timezone Info --%>
<%= if assigns[:browser_timezone_name] do %>
<div class="text-xs text-base-content/60 mt-1">
Browser detected: {@browser_timezone_name} ({@browser_timezone_offset})
</div>
<% end %>
<%!-- Debug button for timezone detection --%>
<div class="mt-2">
<button
type="button"
class="btn btn-sm btn-outline"
onclick="detectAndStoreTimezone(); return false;"
>
🐛 Detect Browser Timezone (Debug)
</button>
<div class="text-xs text-base-content/60 mt-1">
Click if timezone detection isn't working automatically
</div>
</div>
</div>
<:actions>
<.button
phx-disable-with="Updating..."
class="btn-primary transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<PhoenixKitWeb.Components.Core.Icons.icon_user_profile class="w-5 h-5 mr-2" />
Update Profile
</.button>
</:actions>
</.simple_form>
</div>
</div>
<%!-- Password Settings Card --%>
<div class="card bg-base-100 shadow-2xl">
<div class="card-body">
<div class="flex items-center mb-4">
<PhoenixKitWeb.Components.Core.Icons.icon_lock class="w-6 h-6 text-primary mr-3" />
<div>
<h2 class="text-xl font-bold">Password</h2>
<p class="text-sm text-base-content/60">Update your account password</p>
</div>
</div>
<.simple_form
for={@password_form}
id="password_form"
action={Routes.path("/users/log-in?_action=password_updated")}
method="post"
phx-change="validate_password"
phx-submit="update_password"
phx-trigger-action={@trigger_submit}
>
<input
name={@password_form[:email].name}
type="hidden"
id="hidden_user_email"
value={@current_email}
/>
<.input
field={@password_form[:password]}
type="password"
label="New password"
required
>
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_lock class="w-4 h-4 mr-2" />
</:icon>
</.input>
<.input
field={@password_form[:password_confirmation]}
type="password"
label="Confirm new password"
>
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_lock class="w-4 h-4 mr-2" />
</:icon>
</.input>
<.input
field={@password_form[:current_password]}
name="current_password"
type="password"
label="Current password"
id="current_password_for_password"
value={@current_password}
required
>
<:icon>
<PhoenixKitWeb.Components.Core.Icons.icon_lock class="w-4 h-4 mr-2" />
</:icon>
</.input>
<:actions>
<.button
phx-disable-with="Changing..."
class="btn-primary transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<PhoenixKitWeb.Components.Core.Icons.icon_lock class="w-5 h-5 mr-2" />
Change Password
</.button>
</:actions>
</.simple_form>
</div>
</div>
<%!-- Connected Accounts Card --%>
<%= if @oauth_available do %>
<div class="card bg-base-100 shadow-2xl">
<div class="card-body">
<div class="flex items-center mb-4">
<PhoenixKitWeb.Components.Core.Icons.icon_link class="w-6 h-6 text-primary mr-3" />
<div>
<h2 class="text-xl font-bold">Connected Accounts</h2>
<p class="text-sm text-base-content/60">
Manage OAuth providers for quick sign-in
</p>
</div>
</div>
<%!-- Connected Providers List --%>
<%= if length(@oauth_providers) > 0 do %>
<div class="space-y-3 mb-4">
<h3 class="font-semibold text-sm text-base-content/70">Connected Providers</h3>
<%= for provider <- @oauth_providers do %>
<div class="flex items-center justify-between p-3 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
<%= case provider.provider do %>
<% "google" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_google class="w-6 h-6" />
<% "apple" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_apple class="w-6 h-6" />
<% "github" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_github class="w-6 h-6" />
<% "facebook" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_facebook class="w-6 h-6" />
<% _ -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_link class="w-6 h-6" />
<% end %>
<div>
<div class="font-semibold">
{format_provider_name(provider.provider)}
</div>
<div class="text-xs text-base-content/60">
{provider.provider_email || @current_email}
</div>
</div>
</div>
<button
type="button"
phx-click="disconnect_oauth_provider"
phx-value-provider={provider.provider}
class="btn btn-sm btn-outline btn-error"
data-confirm="Are you sure you want to disconnect this account? You won't be able to sign in with it anymore."
>
<PhoenixKitWeb.Components.Core.Icons.icon_trash class="w-4 h-4 mr-1" />
Disconnect
</button>
</div>
<% end %>
</div>
<% else %>
<div class="alert alert-info text-sm mb-4">
<PhoenixKitWeb.Components.Core.Icons.icon_info class="stroke-current shrink-0 h-5 w-5" />
<span>
You don't have any OAuth providers connected yet. Connect one for faster sign-in.
</span>
</div>
<% end %>
<%!-- Available Providers to Connect --%>
<%= if length(@available_providers) > 0 do %>
<div class="space-y-3">
<h3 class="font-semibold text-sm text-base-content/70">
Connect Additional Providers
</h3>
<div class="space-y-2">
<%= for provider <- @available_providers do %>
<button
type="button"
phx-click="connect_oauth_provider"
phx-value-provider={provider}
class="btn btn-outline w-full flex items-center justify-start gap-3 transition-all hover:scale-[1.02]"
>
<%= case provider do %>
<% "google" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_google class="w-5 h-5" />
<span>Connect Google Account</span>
<% "apple" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_apple class="w-5 h-5" />
<span>Connect Apple Account</span>
<% "github" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_github class="w-5 h-5" />
<span>Connect GitHub Account</span>
<% "facebook" -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_facebook class="w-5 h-5" />
<span>Connect Facebook Account</span>
<% _ -> %>
<PhoenixKitWeb.Components.Core.Icons.icon_link class="w-5 h-5" />
<span>Connect {format_provider_name(provider)}</span>
<% end %>
</button>
<% end %>
</div>
</div>
<% end %>
<%!-- Instructions Collapsible --%>
<div class="collapse collapse-arrow bg-base-200 mt-4">
<input type="checkbox" />
<div class="collapse-title text-sm font-medium">
How to connect an OAuth account
</div>
<div class="collapse-content text-sm space-y-2">
<p class="text-base-content/80">
<strong>What is OAuth?</strong>
</p>
<p class="text-base-content/70">
OAuth lets you sign in using your existing Google, Apple, or GitHub account instead
of remembering a password.
</p>
<p class="text-base-content/80 mt-3">
<strong>Requirements:</strong>
</p>
<ul class="list-disc list-inside text-base-content/70 space-y-1">
<li>
Your OAuth account email must match your account email ({@current_email})
</li>
<li>You'll be redirected to the provider to authorize access</li>
<li>After authorization, you'll be brought back here</li>
</ul>
<p class="text-base-content/80 mt-3">
<strong>Security:</strong>
</p>
<p class="text-base-content/70">
We recommend keeping at least one OAuth provider connected OR having a password
set to ensure you can always access your account.
</p>
</div>
</div>
<%!-- Password Warning for OAuth-only Users --%>
<%= if length(@oauth_providers) > 0 and @phoenix_kit_current_user.hashed_password == nil do %>
<div class="alert alert-warning text-sm mt-4">
<PhoenixKitWeb.Components.Core.Icons.icon_warning class="stroke-current shrink-0 h-5 w-5" />
<div>
<div class="font-semibold">No Password Set</div>
<div class="text-xs">
You signed up using OAuth. Consider setting a password above as a backup sign-in
method.
</div>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<%!-- Development Mode Notice --%>
<div :if={show_dev_notice?()} class="alert alert-info">
<PhoenixKitWeb.Components.Core.Icons.icon_info class="stroke-current shrink-0 h-6 w-6" />
<span>
Development mode: Check
<.link href="/dev/mailbox" class="font-semibold underline">mailbox</.link>
for confirmation emails
</span>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>
<script>
// Simple timezone detection - detect once and store in form
function detectAndStoreTimezone() {
try {
// Get browser timezone data
const timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
const now = new Date();
const offsetMinutes = now.getTimezoneOffset();
const offsetHours = Math.round(offsetMinutes / -60);
const offsetString = offsetHours === 0 ? "0" :
(offsetHours > 0 ? "+" + offsetHours : offsetHours.toString());
// Add hidden fields to the form
const form = document.querySelector('#profile_form');
if (form) {
// Remove any existing timezone inputs first
const existingInputs = form.querySelectorAll('input[name="browser_timezone_name"], input[name="browser_timezone_offset"]');
existingInputs.forEach(input => input.remove());
// Add new timezone inputs
const nameInput = document.createElement('input');
nameInput.type = 'hidden';
nameInput.name = 'browser_timezone_name';
nameInput.value = timezoneName;
nameInput.id = 'browser_timezone_name_input';
const offsetInput = document.createElement('input');
offsetInput.type = 'hidden';
offsetInput.name = 'browser_timezone_offset';
offsetInput.value = offsetString;
offsetInput.id = 'browser_timezone_offset_input';
form.appendChild(nameInput);
form.appendChild(offsetInput);
// Trigger form validation to send data to LiveView
const timezoneSelect = form.querySelector('select[name*="user_timezone"]');
if (timezoneSelect) {
const option = timezoneSelect.querySelector(`[value="${offsetString}"]`);
if (option) {
option.selected = true;
}
const changeEvent = new Event('input', { bubbles: true });
timezoneSelect.dispatchEvent(changeEvent);
}
return true;
}
} catch (error) {
// Silent fail - timezone detection is not critical
}
return false;
}
// Detect timezone when page loads
setTimeout(detectAndStoreTimezone, 500);
setTimeout(detectAndStoreTimezone, 1500);
</script>