From b9391de8ca73ffd035d370c5fe037cf791f65b71 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Thu, 19 Mar 2026 21:31:32 +0200 Subject: [PATCH 1/8] Refactor UserSettings component to single card layout - Consolidated 4 separate cards (Profile, Email, Password, OAuth) into one unified card - Added dividers between sections for visual separation - Moved avatar section to top with horizontal layout - Simplified button labels and reduced visual clutter - Reduced file from 1470 to 954 lines Co-Authored-By: Claude Opus 4.6 --- .../live/components/user_settings.ex | 799 +++++++----------- 1 file changed, 292 insertions(+), 507 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index ababf9e70..c88211ded 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -638,530 +638,315 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do @impl Phoenix.LiveComponent def render(assigns) do ~H""" -
-
- <%!-- Left Column - Profile --%> -
- <%!-- Profile Information Card --%> - <%= if :profile in @sections do %> -
-
-

- <.icon name="hero-user" class="w-5 h-5" /> Profile Information -

- - <%!-- Success Message --%> - <%= if @profile_success_message do %> -
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" /> - {@profile_success_message} -
- <% end %> - - <%!-- Avatar Upload Section --%> -
- - -
- <%!-- Avatar Preview --%> -
- <%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %> - <% avatar_url = - PhoenixKit.Modules.Storage.URLSigner.signed_url( - get_in(@user.custom_fields, ["avatar_file_uuid"]), - "thumbnail" - ) %> - Avatar - <% else %> -
- - {String.upcase(String.at(@user.email, 0))} - -
- <% end %> -
- - <%!-- Upload Controls --%> -
- <.file_upload - upload={@uploads.avatar} - variant="button" - label="Choose Profile Picture" - /> - -

- Upload a profile picture (max 10MB). Accepts JPG, PNG, GIF. -

- - <%!-- Success Message --%> - <%= if @last_uploaded_avatar_uuid do %> -
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" /> - Avatar uploaded successfully! -
- <% end %> - - <%!-- Avatar Error Message --%> - <%= if @avatar_error_message do %> -
- <.icon - name="hero-exclamation-triangle" - class="stroke-current shrink-0 h-5 w-5" - /> - {@avatar_error_message} -
- <% end %> -
-
- - <%!-- Divider after avatar section --%> -
+
+
+

+ <.icon name="hero-user-circle" class="w-6 h-6" /> Account Settings +

+ + <%!-- Profile Section --%> + <%= if :profile in @sections do %> +
+ <%!-- Avatar Section --%> +
+ <%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %> + <% avatar_url = + PhoenixKit.Modules.Storage.URLSigner.signed_url( + get_in(@user.custom_fields, ["avatar_file_uuid"]), + "thumbnail" + ) %> + Avatar + <% else %> +
+ + {String.upcase(String.at(@user.email, 0))} +
+ <% end %> +
+ <.file_upload + upload={@uploads.avatar} + variant="button" + label="Change Profile Picture" + /> +

JPG, PNG, GIF up to 10MB

+
+
+ + <%!-- Avatar Messages --%> + <%= if @last_uploaded_avatar_uuid do %> +
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" /> + Avatar uploaded successfully! +
+ <% end %> + <%= if @avatar_error_message do %> +
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" /> + {@avatar_error_message} +
+ <% end %> + + <%!-- Profile Form --%> + <.simple_form + for={@profile_form} + id={"#{@id}-profile-form"} + phx-submit="update_profile" + phx-change="validate_profile" + phx-target={@myself} + > +
+ <.input + field={@profile_form[:first_name]} + type="text" + label="First Name" + /> + <.input + field={@profile_form[:last_name]} + type="text" + label="Last Name" + /> +
- <.simple_form - for={@profile_form} - id={"#{@id}-profile-form"} - phx-submit="update_profile" - phx-change="validate_profile" - phx-target={@myself} - > -
+ <%!-- Custom Fields --%> + <%= for field_def <- @custom_field_definitions do %> + <%= case field_def["type"] do %> + <% "text" -> %> <.input - field={@profile_form[:first_name]} + name={"custom_fields[#{field_def["key"]}]"} type="text" - label="First Name" + label={field_def["label"]} + value={get_in(@user.custom_fields, [field_def["key"]]) || field_def["default"]} + required={field_def["required"]} /> + <% "select" -> %> + <.select + name={"custom_fields[#{field_def["key"]}]"} + label={field_def["label"]} + options={Enum.map(field_def["options"], &{&1, &1})} + value={get_in(@user.custom_fields, [field_def["key"]]) || field_def["default"]} + required={field_def["required"]} + /> + <% _ -> %> <.input - field={@profile_form[:last_name]} + name={"custom_fields[#{field_def["key"]}]"} type="text" - label="Last Name" - /> -
- - <%!-- Custom Fields Section --%> - <%= if length(@custom_field_definitions) > 0 do %> -
Additional Information
- - <%= 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(@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(@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(@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(@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(@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(@user.custom_fields, [field_def["key"]]) || - field_def["default"] - } - required={field_def["required"]} - /> - <% "select" -> %> - <.select - name={"custom_fields[#{field_def["key"]}]"} - label={field_def["label"]} - options={Enum.map(field_def["options"], &{&1, &1})} - value={ - get_in(@user.custom_fields, [field_def["key"]]) || - field_def["default"] - } - required={field_def["required"]} - /> - <% _ -> %> - <%!-- Fallback for unknown field types --%> - <.input - name={"custom_fields[#{field_def["key"]}]"} - type="text" - label={field_def["label"]} - value={ - get_in(@user.custom_fields, [field_def["key"]]) || - field_def["default"] - } - required={field_def["required"]} - /> - <% end %> - <% end %> - <% end %> - -
- <.select - field={@profile_form[:user_timezone]} - label="Personal Timezone" - options={@timezone_options} + label={field_def["label"]} + value={get_in(@user.custom_fields, [field_def["key"]]) || field_def["default"]} + required={field_def["required"]} /> - - <%!-- Timezone Mismatch Warning --%> - <%= if assigns[:timezone_mismatch_warning] do %> -
- <.icon - name="hero-exclamation-triangle" - class="stroke-current shrink-0 h-5 w-5" - /> -
-
Timezone Mismatch Detected
-
- {@timezone_mismatch_warning} -
-
-
- <% end %> - - <%!-- Browser Timezone Info --%> - <%= if assigns[:browser_timezone_name] do %> -
- Browser detected: {@browser_timezone_name} ({@browser_timezone_offset}) -
- <% end %> - - <%!-- Debug button for timezone detection --%> -
- -
- Click if timezone detection isn't working automatically -
-
-
- <:actions> - <.button - phx-disable-with="Updating..." - class="btn-primary" - > - <.icon name="hero-user" class="w-4 h-4 mr-2" /> Update Profile - - - -
-
- <% end %> - - <%!-- Email Settings Card --%> - <%= if :email in @sections do %> -
-
-

- <.icon name="hero-envelope" class="w-5 h-5" /> Email Address -

-

Change your account email address

- - <%!-- Email Success Message --%> - <%= if @email_success_message do %> -
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" /> - {@email_success_message} -
<% end %> - - <%!-- Email Error Message --%> - <%= if @email_error_message do %> -
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" /> - {@email_error_message} -
- <% end %> - - <.simple_form - for={@email_form} - id={"#{@id}-email-form"} - phx-submit="update_email" - phx-change="validate_email" - phx-target={@myself} - > - <.input - field={@email_form[:email]} - type="email" - label="Email" - required - /> - <.input - field={@email_form[:current_password]} - name="current_password" - id={"#{@id}-current-password-for-email"} - type="password" - label="Current password" - value={@email_form_current_password} - required - /> - <:actions> - <.button - phx-disable-with="Changing..." - class="btn-primary" - > - <.icon name="hero-envelope" class="w-4 h-4 mr-2" /> Change Email - - - + <% end %> + + <.select + field={@profile_form[:user_timezone]} + label="Timezone" + options={@timezone_options} + /> + + <:actions> + <.button phx-disable-with="Updating..." class="btn-primary"> + Update Profile + + + +
+ + <%!-- Divider --%> +
+ <% end %> + + <%!-- Email Section --%> + <%= if :email in @sections do %> +
+

+ <.icon name="hero-envelope" class="w-5 h-5 text-primary" /> Email Address +

+ + <%= if @email_success_message do %> +
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" /> + {@email_success_message}
-
- <% end %> - - <%!-- Password Settings Card --%> - <%= if :password in @sections do %> -
-
-

- <.icon name="hero-lock-closed" class="w-5 h-5" /> Password -

-

Update your account password

- - <%!-- Password Success Message --%> - <%= if @password_success_message do %> -
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" /> - {@password_success_message} -
- <% end %> - - <%!-- Password Error Message --%> - <%= if @password_error_message do %> -
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" /> - {@password_error_message} -
- <% end %> - - <.simple_form - for={@password_form} - id={"#{@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} - phx-target={@myself} - > - - <.input - field={@password_form[:password]} - type="password" - label="New password" - required - /> - <.input - field={@password_form[:password_confirmation]} - type="password" - label="Confirm new password" - /> - <.input - field={@password_form[:current_password]} - name="current_password" - type="password" - label="Current password" - id={"#{@id}-current-password-for-password"} - value={@current_password} - required - /> - <:actions> - <.button - phx-disable-with="Changing..." - class="btn-primary" - > - <.icon name="hero-lock-closed" class="w-4 h-4 mr-2" /> Change Password - - - + <% end %> + <%= if @email_error_message do %> +
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" /> + {@email_error_message}
-
- <% end %> -
- - <%!-- Right Column --%> -
- <%!-- Connected Accounts Card --%> - <%= if :oauth in @sections and @oauth_available do %> -
-
-

- <.icon name="hero-link" class="w-5 h-5" /> Connected Accounts -

-

- Manage OAuth providers for quick sign-in -

- - <%!-- OAuth Success Message --%> - <%= if @oauth_success_message do %> -
- <.icon name="hero-check" class="stroke-current shrink-0 h-5 w-5" /> - {@oauth_success_message} -
- <% end %> - - <%!-- OAuth Error Message --%> - <%= if @oauth_error_message do %> -
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" /> - {@oauth_error_message} -
- <% end %> - - <%!-- Connected Providers List --%> - <%= if length(@oauth_providers) > 0 do %> -
-

Connected Providers

- <%= for provider <- @oauth_providers do %> -
-
- <%= case provider.provider do %> - <% "google" -> %> - <.icon name="hero-globe-alt" class="w-6 h-6" /> - <% "apple" -> %> - <.icon name="hero-device-phone-mobile" class="w-6 h-6" /> - <% "github" -> %> - <.icon name="hero-code-bracket" class="w-6 h-6" /> - <% _ -> %> - <.icon name="hero-link" class="w-6 h-6" /> - <% end %> -
-
- {format_provider_name(provider.provider)} -
-
- {provider.provider_email || @current_email} -
-
-
- -
- <% end %> -
- <% else %> -
- <.icon name="hero-information-circle" class="stroke-current shrink-0 h-5 w-5" /> - - You don't have any OAuth providers connected yet. Connect one for faster sign-in. - -
- <% end %> - - <%!-- Available Providers to Connect --%> - <%= if length(@available_providers) > 0 do %> -
-

- Connect Additional Providers -

-
- <%= for provider <- @available_providers do %> - + <% end %> + + <.simple_form + for={@email_form} + id={"#{@id}-email-form"} + phx-submit="update_email" + phx-change="validate_email" + phx-target={@myself} + > + <.input + field={@email_form[:email]} + type="email" + label="New Email" + required + /> + <.input + field={@email_form[:current_password]} + name="current_password" + type="password" + label="Current Password" + value={@email_form_current_password} + required + /> + <:actions> + <.button phx-disable-with="Changing..." class="btn-primary"> + Change Email + + + +
+ +
+ <% end %> + + <%!-- Password Section --%> + <%= if :password in @sections do %> +
+

+ <.icon name="hero-lock-closed" class="w-5 h-5 text-primary" /> Password +

+ + <%= if @password_success_message do %> +
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" /> + {@password_success_message} +
+ <% end %> + <%= if @password_error_message do %> +
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" /> + {@password_error_message} +
+ <% end %> + + <.simple_form + for={@password_form} + id={"#{@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} + phx-target={@myself} + > + + <.input + field={@password_form[:password]} + type="password" + label="New Password" + required + /> + <.input + field={@password_form[:password_confirmation]} + type="password" + label="Confirm New Password" + /> + <.input + field={@password_form[:current_password]} + name="current_password" + type="password" + label="Current Password" + value={@current_password} + required + /> + <:actions> + <.button phx-disable-with="Changing..." class="btn-primary"> + Change Password + + + +
+ +
+ <% end %> + + <%!-- OAuth Section --%> + <%= if :oauth in @sections and @oauth_available do %> +
+

+ <.icon name="hero-link" class="w-5 h-5 text-primary" /> Connected Accounts +

+ + <%= if @oauth_success_message do %> +
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" /> + {@oauth_success_message} +
+ <% end %> + <%= if @oauth_error_message do %> +
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" /> + {@oauth_error_message} +
+ <% end %> + + <%!-- Connected Providers --%> + <%= if length(@oauth_providers) > 0 do %> +
+ <%= for provider <- @oauth_providers do %> +
+
+ <%= case provider.provider do %> + <% "google" -> %> + <.icon name="hero-globe-alt" class="w-5 h-5" /> + <% "github" -> %> + <.icon name="hero-code-bracket" class="w-5 h-5" /> + <% _ -> %> + <.icon name="hero-link" class="w-5 h-5" /> <% end %> + {format_provider_name(provider.provider)}
+
<% end %> - - <%!-- Password Warning for OAuth-only Users --%> - <%= if length(@oauth_providers) > 0 and @user.hashed_password == nil do %> -
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-5 w-5" /> -
-
No Password Set
-
- You signed up using OAuth. Consider setting a password above as a backup sign-in method. -
-
-
+
+ <% end %> + + <%!-- Available Providers --%> + <%= if length(@available_providers) > 0 do %> +
+ <%= for provider <- @available_providers do %> + <% end %>
-
- <% end %> -
+ <% end %> +
+ <% end %>
""" From d42289abd52742bbe1be8e74761f5e8305d00f6b Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Thu, 19 Mar 2026 21:37:45 +0200 Subject: [PATCH 2/8] Add toggle buttons for Email and Password forms in UserSettings - Email and Password forms are now hidden by default - Users click "Change Email" or "Change Password" buttons to reveal forms - Toggle shows "Cancel" with X icon when form is open - Simplifies the UI by reducing visual clutter for common operations Co-Authored-By: Claude Opus 4.6 --- .../live/components/user_settings.ex | 192 +++++++++++------- 1 file changed, 119 insertions(+), 73 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index c88211ded..ac1bc314e 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -119,6 +119,8 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do CustomFields.list_user_accessible_field_definitions() end) |> assign_new(:last_uploaded_avatar_uuid, fn -> nil end) + |> assign_new(:show_email_form, fn -> false end) + |> assign_new(:show_password_form, fn -> false end) |> maybe_allow_upload() {:ok, socket} @@ -448,6 +450,14 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do {:noreply, cancel_upload(socket, :avatar, ref)} end + def handle_event("toggle_email_form", _params, socket) do + {:noreply, assign(socket, :show_email_form, not socket.assigns.show_email_form)} + end + + def handle_event("toggle_password_form", _params, socket) do + {:noreply, assign(socket, :show_password_form, not socket.assigns.show_password_form)} + end + # Private helpers defp check_timezone_mismatch(socket, selected_timezone) do @@ -763,9 +773,25 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do <%!-- Email Section --%> <%= if :email in @sections do %>
-

- <.icon name="hero-envelope" class="w-5 h-5 text-primary" /> Email Address -

+
+

+ <.icon name="hero-envelope" class="w-5 h-5 text-primary" /> Email Address +

+ +
+ +
{@current_email}
<%= if @email_success_message do %>
@@ -780,33 +806,35 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
<% end %> - <.simple_form - for={@email_form} - id={"#{@id}-email-form"} - phx-submit="update_email" - phx-change="validate_email" - phx-target={@myself} - > - <.input - field={@email_form[:email]} - type="email" - label="New Email" - required - /> - <.input - field={@email_form[:current_password]} - name="current_password" - type="password" - label="Current Password" - value={@email_form_current_password} - required - /> - <:actions> - <.button phx-disable-with="Changing..." class="btn-primary"> - Change Email - - - + <%= if @show_email_form do %> + <.simple_form + for={@email_form} + id={"#{@id}-email-form"} + phx-submit="update_email" + phx-change="validate_email" + phx-target={@myself} + > + <.input + field={@email_form[:email]} + type="email" + label="New Email" + required + /> + <.input + field={@email_form[:current_password]} + name="current_password" + type="password" + label="Current Password" + value={@email_form_current_password} + required + /> + <:actions> + <.button phx-disable-with="Changing..." class="btn-primary"> + Update Email + + + + <% end %>
@@ -815,9 +843,25 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do <%!-- Password Section --%> <%= if :password in @sections do %>
-

- <.icon name="hero-lock-closed" class="w-5 h-5 text-primary" /> Password -

+
+

+ <.icon name="hero-lock-closed" class="w-5 h-5 text-primary" /> Password +

+ +
+ +
••••••••
<%= if @password_success_message do %>
@@ -832,46 +876,48 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
<% end %> - <.simple_form - for={@password_form} - id={"#{@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} - phx-target={@myself} - > - - <.input - field={@password_form[:password]} - type="password" - label="New Password" - required - /> - <.input - field={@password_form[:password_confirmation]} - type="password" - label="Confirm New Password" - /> - <.input - field={@password_form[:current_password]} - name="current_password" - type="password" - label="Current Password" - value={@current_password} - required - /> - <:actions> - <.button phx-disable-with="Changing..." class="btn-primary"> - Change Password - - - + <%= if @show_password_form do %> + <.simple_form + for={@password_form} + id={"#{@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} + phx-target={@myself} + > + + <.input + field={@password_form[:password]} + type="password" + label="New Password" + required + /> + <.input + field={@password_form[:password_confirmation]} + type="password" + label="Confirm New Password" + /> + <.input + field={@password_form[:current_password]} + name="current_password" + type="password" + label="Current Password" + value={@current_password} + required + /> + <:actions> + <.button phx-disable-with="Changing..." class="btn-primary"> + Update Password + + + + <% end %>
From dbeda14e1d15d60413f3ab32e2293501ee32ab8d Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Thu, 19 Mar 2026 21:47:07 +0200 Subject: [PATCH 3/8] Compact profile section layout with smaller avatar - Avatar reduced to 64px (w-16 h-16) and stacked with upload button - Name fields (first/last) moved beside the avatar column - Cleaner, more compact top section of the profile form Co-Authored-By: Claude Opus 4.6 --- .../live/components/user_settings.ex | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index ac1bc314e..c891da664 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -657,51 +657,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do <%!-- Profile Section --%> <%= if :profile in @sections do %>
- <%!-- Avatar Section --%> -
- <%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %> - <% avatar_url = - PhoenixKit.Modules.Storage.URLSigner.signed_url( - get_in(@user.custom_fields, ["avatar_file_uuid"]), - "thumbnail" - ) %> - Avatar - <% else %> -
- - {String.upcase(String.at(@user.email, 0))} - -
- <% end %> -
- <.file_upload - upload={@uploads.avatar} - variant="button" - label="Change Profile Picture" - /> -

JPG, PNG, GIF up to 10MB

-
-
- - <%!-- Avatar Messages --%> - <%= if @last_uploaded_avatar_uuid do %> -
- <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" /> - Avatar uploaded successfully! -
- <% end %> - <%= if @avatar_error_message do %> -
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" /> - {@avatar_error_message} -
- <% end %> - - <%!-- Profile Form --%> + <%!-- Profile Form with Avatar --%> <.simple_form for={@profile_form} id={"#{@id}-profile-form"} @@ -709,19 +665,63 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do phx-change="validate_profile" phx-target={@myself} > -
- <.input - field={@profile_form[:first_name]} - type="text" - label="First Name" - /> - <.input - field={@profile_form[:last_name]} - type="text" - label="Last Name" - /> +
+ <%!-- Avatar Column --%> +
+ <%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %> + <% avatar_url = + PhoenixKit.Modules.Storage.URLSigner.signed_url( + get_in(@user.custom_fields, ["avatar_file_uuid"]), + "thumbnail" + ) %> + Avatar + <% else %> +
+ + {String.upcase(String.at(@user.email, 0))} + +
+ <% end %> + <.file_upload + upload={@uploads.avatar} + variant="button" + label="Upload" + /> +
+ + <%!-- Name Fields Column --%> +
+ <.input + field={@profile_form[:first_name]} + type="text" + label="First Name" + /> + <.input + field={@profile_form[:last_name]} + type="text" + label="Last Name" + /> +
+ <%!-- Avatar Messages --%> + <%= if @last_uploaded_avatar_uuid do %> +
+ <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" /> + Avatar uploaded successfully! +
+ <% end %> + <%= if @avatar_error_message do %> +
+ <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" /> + {@avatar_error_message} +
+ <% end %> + <%!-- Custom Fields --%> <%= for field_def <- @custom_field_definitions do %> <%= case field_def["type"] do %> From 1e87740eaaba380e996d8a38852a92d83f8493e8 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Thu, 19 Mar 2026 21:52:55 +0200 Subject: [PATCH 4/8] Simplify profile section to avatar + upload + 2 name fields - Avatar now 96px tall (h-24) to span both input heights - Upload button positioned right of avatar - Removed custom fields, timezone, and avatar messages for cleaner UI - Only first name and last name inputs remain Co-Authored-By: Claude Opus 4.6 --- .../live/components/user_settings.ex | 66 +++---------------- 1 file changed, 8 insertions(+), 58 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index c891da664..c445f6023 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -665,9 +665,9 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do phx-change="validate_profile" phx-target={@myself} > -
- <%!-- Avatar Column --%> -
+
+ <%!-- Avatar Section --%> +
<%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %> <% avatar_url = PhoenixKit.Modules.Storage.URLSigner.signed_url( @@ -677,11 +677,11 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do Avatar <% else %> -
- +
+ {String.upcase(String.at(@user.email, 0))}
@@ -693,8 +693,8 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do />
- <%!-- Name Fields Column --%> -
+ <%!-- Name Fields --%> +
<.input field={@profile_form[:first_name]} type="text" @@ -708,56 +708,6 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
- <%!-- Avatar Messages --%> - <%= if @last_uploaded_avatar_uuid do %> -
- <.icon name="hero-check" class="stroke-current shrink-0 h-4 w-4" /> - Avatar uploaded successfully! -
- <% end %> - <%= if @avatar_error_message do %> -
- <.icon name="hero-exclamation-triangle" class="stroke-current shrink-0 h-4 w-4" /> - {@avatar_error_message} -
- <% end %> - - <%!-- Custom Fields --%> - <%= 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(@user.custom_fields, [field_def["key"]]) || field_def["default"]} - required={field_def["required"]} - /> - <% "select" -> %> - <.select - name={"custom_fields[#{field_def["key"]}]"} - label={field_def["label"]} - options={Enum.map(field_def["options"], &{&1, &1})} - value={get_in(@user.custom_fields, [field_def["key"]]) || field_def["default"]} - required={field_def["required"]} - /> - <% _ -> %> - <.input - name={"custom_fields[#{field_def["key"]}]"} - type="text" - label={field_def["label"]} - value={get_in(@user.custom_fields, [field_def["key"]]) || field_def["default"]} - required={field_def["required"]} - /> - <% end %> - <% end %> - - <.select - field={@profile_form[:user_timezone]} - label="Timezone" - options={@timezone_options} - /> - <:actions> <.button phx-disable-with="Updating..." class="btn-primary"> Update Profile From a0f5616f366428ee4beb08f23eb0562ca3aed9b6 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 01:37:54 +0200 Subject: [PATCH 5/8] Redesign UserSettings profile section and fix custom fields Profile section redesign: - Larger circular avatar (160px) with upload button below - Two-column layout for First/Last Name with Username field - Added custom fields rendering with text and select support - Select dropdowns use index-based values per system design Layout improvements: - All action buttons aligned to right (ml-auto) - Strategic dividers between sections - Removed unnecessary margins for cleaner look Bug fixes: - Custom fields now save correctly by reading from profile_form[user][custom_fields] instead of params["custom_fields"] - Select dropdowns properly compare index values Co-Authored-By: Claude Opus 4.6 --- .../live/components/user_settings.ex | 108 +++++++++++++----- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index c445f6023..5f3797dac 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -255,8 +255,11 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do socket end + # Custom fields are nested under profile_form[user][custom_fields] + custom_fields_data = get_in(params, ["profile_form", "user", "custom_fields"]) + merged_params = - case params["custom_fields"] do + case custom_fields_data do custom_fields when is_map(custom_fields) -> Map.put(user_params, "custom_fields", custom_fields) @@ -286,8 +289,11 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do %{"user" => user_params} = params user = socket.assigns.user + # Custom fields are nested under profile_form[user][custom_fields] + custom_fields_data = get_in(params, ["profile_form", "user", "custom_fields"]) + merged_params = - case params["custom_fields"] do + case custom_fields_data do custom_fields when is_map(custom_fields) -> existing_avatar = get_in(user.custom_fields, ["avatar_file_uuid"]) @@ -656,7 +662,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do <%!-- Profile Section --%> <%= if :profile in @sections do %> -
+
<%!-- Profile Form with Avatar --%> <.simple_form for={@profile_form} @@ -667,7 +673,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do >
<%!-- Avatar Section --%> -
+
<%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %> <% avatar_url = PhoenixKit.Modules.Storage.URLSigner.signed_url( @@ -677,11 +683,11 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do Avatar <% else %> -
- +
+ {String.upcase(String.at(@user.email, 0))}
@@ -694,7 +700,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
<%!-- Name Fields --%> -
+
<.input field={@profile_form[:first_name]} type="text" @@ -705,24 +711,72 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do type="text" label="Last Name" /> +
+ <.input + field={@profile_form[:username]} + type="text" + label="Username" + /> +
+
+ <%= for field <- @custom_field_definitions do %> + <% field_name = "profile_form[user][custom_fields][#{field["key"]}]" %> + <% field_id = "profile_form_user_custom_fields_#{field["key"]}" %> + <% field_value = + get_in(@user.custom_fields, [field["key"]]) || field["default"] || "" %> +
+ + <%= case field["type"] do %> + <% "select" -> %> + + <% _ -> %> + + <% end %> +
+ <% end %>
<:actions> - <.button phx-disable-with="Updating..." class="btn-primary"> - Update Profile - +
+ <.button phx-disable-with="Updating..." class="btn-primary"> + Update Profile + +
+
- - <%!-- Divider --%> -
<% end %> <%!-- Email Section --%> <%= if :email in @sections do %> -
+

<.icon name="hero-envelope" class="w-5 h-5 text-primary" /> Email Address @@ -779,20 +833,22 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do required /> <:actions> - <.button phx-disable-with="Changing..." class="btn-primary"> - Update Email - +
+ <.button phx-disable-with="Changing..." class="btn-primary"> + Update Email + +
<% end %>

- -
<% end %> +
+ <%!-- Password Section --%> <%= if :password in @sections do %> -
+

<.icon name="hero-lock-closed" class="w-5 h-5 text-primary" /> Password @@ -862,15 +918,15 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do required /> <:actions> - <.button phx-disable-with="Changing..." class="btn-primary"> - Update Password - +
+ <.button phx-disable-with="Changing..." class="btn-primary"> + Update Password + +
<% end %>

- -
<% end %> <%!-- OAuth Section --%> From 1b23c709235d5d1dd0b3362cb303136e55a42394 Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 01:44:47 +0200 Subject: [PATCH 6/8] Add responsive mobile layout to UserSettings profile section - Avatar centered on its own row on mobile - All fields (First Name, Last Name, Username, custom fields) full-width below - Desktop maintains side-by-side layout with avatar on left - Uses Tailwind responsive classes (lg:) for breakpoints Co-Authored-By: Claude Opus 4.6 --- lib/phoenix_kit_web/live/components/user_settings.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index 5f3797dac..9fc591ece 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -671,9 +671,9 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do phx-change="validate_profile" phx-target={@myself} > -
+
<%!-- Avatar Section --%> -
+
<%= if get_in(@user.custom_fields, ["avatar_file_uuid"]) do %> <% avatar_url = PhoenixKit.Modules.Storage.URLSigner.signed_url( @@ -700,7 +700,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do
<%!-- Name Fields --%> -
+
<.input field={@profile_form[:first_name]} type="text" @@ -711,7 +711,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do type="text" label="Last Name" /> -
+
<.input field={@profile_form[:username]} type="text" @@ -724,7 +724,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do <% field_id = "profile_form_user_custom_fields_#{field["key"]}" %> <% field_value = get_in(@user.custom_fields, [field["key"]]) || field["default"] || "" %> -
+
From dbae30f6f220cf1dc6e583bf8c60d6382a6d705b Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 01:48:04 +0200 Subject: [PATCH 7/8] Remove bottom margin from Account Settings header Co-Authored-By: Claude Opus 4.6 --- lib/phoenix_kit_web/live/components/user_settings.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix_kit_web/live/components/user_settings.ex b/lib/phoenix_kit_web/live/components/user_settings.ex index 9fc591ece..40dba7685 100644 --- a/lib/phoenix_kit_web/live/components/user_settings.ex +++ b/lib/phoenix_kit_web/live/components/user_settings.ex @@ -656,7 +656,7 @@ defmodule PhoenixKitWeb.Live.Components.UserSettings do ~H"""
-

+

<.icon name="hero-user-circle" class="w-6 h-6" /> Account Settings

From f572bc469f8e766b1c4519dbdb75d6a7a0eff30b Mon Sep 17 00:00:00 2001 From: Alexander Don Date: Fri, 20 Mar 2026 01:59:26 +0200 Subject: [PATCH 8/8] Add Dialyzer directives for MapSet opaqueness warnings Suppress known MapSet type warnings in sync module functions: - get_table_dependencies/2-3 - topo_sort/4 - visit_node/5 These are false positives from Dialyzer's handling of MapSet's internal opaque type representation across recursive calls. Co-Authored-By: Claude Opus 4.6 --- lib/modules/sync/web/connections_live.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/modules/sync/web/connections_live.ex b/lib/modules/sync/web/connections_live.ex index 1111486cd..4c2ab3538 100644 --- a/lib/modules/sync/web/connections_live.ex +++ b/lib/modules/sync/web/connections_live.ex @@ -1116,6 +1116,8 @@ defmodule PhoenixKit.Modules.Sync.Web.ConnectionsLive do end # Get all FK dependencies for a table (recursive) + @dialyzer {:nowarn_function, get_table_dependencies: 2} + @dialyzer {:nowarn_function, get_table_dependencies: 3} defp get_table_dependencies(table_name, tables) do get_table_dependencies(table_name, tables, MapSet.new()) |> MapSet.to_list() @@ -1159,6 +1161,7 @@ defmodule PhoenixKit.Modules.Sync.Web.ConnectionsLive do topo_sort(graph) end + @dialyzer {:nowarn_function, topo_sort: 4} defp topo_sort(graph) do topo_sort(graph, Map.keys(graph), [], MapSet.new()) end @@ -1174,6 +1177,7 @@ defmodule PhoenixKit.Modules.Sync.Web.ConnectionsLive do end end + @dialyzer {:nowarn_function, visit_node: 5} defp visit_node(graph, node, sorted, visited, path) do if MapSet.member?(visited, node) do {sorted, visited}