From 90ae39091cd10b91f85cf4fb6e85bf0d7a77fc06 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 18 Jan 2026 10:35:29 -0600 Subject: [PATCH] refactor user settings to use tab navigation - Add tab navigation UI with Personal, Account, Security, API, Notifications tabs - Add active_tab state management in LiveView - Add switch_tab event handler - Show only active tab content (like Tailwind Plus template) - Remove section prefixes since each is now on separate tab - Default to Personal tab on mount --- lib/towerops_web/live/user_settings_live.ex | 1083 ++++++++++--------- 1 file changed, 593 insertions(+), 490 deletions(-) diff --git a/lib/towerops_web/live/user_settings_live.ex b/lib/towerops_web/live/user_settings_live.ex index b73dbc03..75ebd6fb 100644 --- a/lib/towerops_web/live/user_settings_live.ex +++ b/lib/towerops_web/live/user_settings_live.ex @@ -14,6 +14,7 @@ defmodule ToweropsWeb.UserSettingsLive do socket = socket |> assign(:page_title, "Account Settings") + |> assign(:active_tab, "personal") |> assign_changesets() |> assign_profile_form() |> assign_credentials() @@ -27,6 +28,11 @@ defmodule ToweropsWeb.UserSettingsLive do {:ok, socket} end + @impl true + def handle_event("switch_tab", %{"tab" => tab}, socket) do + {:noreply, assign(socket, :active_tab, tab)} + end + @impl true def handle_event("update_profile", %{"user" => user_params}, socket) do user = socket.assigns.current_scope.user @@ -240,303 +246,438 @@ defmodule ToweropsWeb.UserSettingsLive do

+
+ +
+
-
-
-

- Personal Information -

-

- Update your name, avatar, and timezone preferences. -

-
+ <%= if @active_tab == "personal" do %> +
+
+

+ Personal Information +

+

+ Update your name, avatar, and timezone preferences. +

+
- <.form - for={@profile_form} - phx-submit="update_profile" - id="update_profile" - class="md:col-span-2" - > -
-
- -
- -
-
- -
- -
- -
-

- Enter a URL to an avatar image or use a service like Gravatar. -

-
- -
- -
- -
+
+ +
+ - +
+
+ +
+ +
+ +
+

+ Enter a URL to an avatar image or use a service like Gravatar. +

+
+ +
+ +
+ + +
-
-
- -
- -
- - -
-
-

Account - Email

-

- Update your email address. You'll receive a confirmation link to verify the new address. -

-
- - <.form - for={@email_form} - phx-submit="update_email" - id="update_email" - class="md:col-span-2" - > -
-
- -
- -
-
-
- -
- -
- -
- - -
-
-

- Account - Password -

-

- Update your password. You'll be logged out and need to sign in again with your new password. -

-
- - <.form - for={@password_form} - phx-submit="update_password" - id="update_password" - class="md:col-span-2" - > -
-
-
- -
- -
- -
-
-
- -
- -
- -
- -
-
-

API Tokens

-

- Create API tokens to access Towerops programmatically. Tokens are scoped to a specific organization. -

+
+ <% end %> + + + <%= if @active_tab == "account" do %> + +
+
+

Email Address

+

+ Update your email address. You'll receive a confirmation link to verify the new address. +

+
-
- <%= if Enum.empty?(@api_tokens) do %> -
- <.icon name="hero-code-bracket" class="mx-auto h-12 w-12 text-gray-400" /> -

- No API tokens -

-

- Get started by creating a new API token. -

-
+ <.form + for={@email_form} + phx-submit="update_email" + id="update_email" + class="md:col-span-2" + > +
+
+ +
+ +
+
+
+ +
+ +
+ +
+ + +
+
+

+ Change Password +

+

+ Update your password. You'll be logged out and need to sign in again with your new password. +

+
+ + <.form + for={@password_form} + phx-submit="update_password" + id="update_password" + class="md:col-span-2" + > +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ <% end %> + + + <%= if @active_tab == "api" do %> + +
+
+

API Tokens

+

+ Create API tokens to access Towerops programmatically. Tokens are scoped to a specific organization. +

+
+ +
+ <%= if Enum.empty?(@api_tokens) do %> +
+ <.icon name="hero-code-bracket" class="mx-auto h-12 w-12 text-gray-400" /> +

+ No API tokens +

+

+ Get started by creating a new API token. +

+
+ +
+
+ <% else %> +
    + <%= for token <- @api_tokens do %> +
  • +
    +
    +

    + {token.name} +

    +
    +
    +

    {token.organization.name}

    + + + +

    + <%= if token.last_used_at do %> + Last used {Calendar.strftime(token.last_used_at, "%B %d, %Y")} + <% else %> + Never used + <% end %> +

    +
    +
    + +
  • + <% end %> +
+ +
-
- <% else %> -
    - <%= for token <- @api_tokens do %> -
  • -
    -
    -

    - {token.name} -

    -
    -
    -

    {token.organization.name}

    - - - -

    - <%= if token.last_used_at do %> - Last used {Calendar.strftime(token.last_used_at, "%B %d, %Y")} - <% else %> - Never used - <% end %> -

    -
    -
    - -
  • - <% end %> -
- -
- -
- <% end %> + <% end %> +
-
+ <% end %> - -
-
-

- Notifications - Mobile Devices -

-

- Manage mobile devices that receive push notifications for alerts. -

-
+ + <%= if @active_tab == "notifications" do %> + +
+
+

+ Mobile Devices +

+

+ Manage mobile devices that receive push notifications for alerts. +

+
-
- <%= if Enum.empty?(@mobile_sessions) do %> -
- <.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" /> -

- No mobile devices -

-

- Add a mobile device to receive push notifications for alerts. -

-
+
+ <%= if Enum.empty?(@mobile_sessions) do %> +
+ <.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" /> +

+ No mobile devices +

+

+ Add a mobile device to receive push notifications for alerts. +

+
+ <.link + navigate={~p"/mobile/qr-login"} + class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400" + > + <.icon name="hero-qr-code" class="-ml-0.5 h-5 w-5" /> Add Device + +
+
+ <% else %> +
    + <%= for session <- @mobile_sessions do %> +
  • +
    +
    +

    + {session.device_name || "Unknown Device"} +

    + <%= if session.alerts_enabled do %> + + <.icon name="hero-bell" class="h-3 w-3" /> Alerts On + + <% else %> + + <.icon name="hero-bell-slash" class="h-3 w-3" /> Alerts Off + + <% end %> +
    +
    +

    {session.device_os} • {session.app_version}

    + + + +

    Last used {Calendar.strftime(session.last_used_at, "%B %d, %Y")}

    +
    +
    +
    + + +
    +
  • + <% end %> +
+ +
<.link navigate={~p"/mobile/qr-login"} class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400" @@ -625,99 +782,89 @@ defmodule ToweropsWeb.UserSettingsLive do <.icon name="hero-qr-code" class="-ml-0.5 h-5 w-5" /> Add Device
-
- <% else %> -
    - <%= for session <- @mobile_sessions do %> -
  • -
    -
    -

    - {session.device_name || "Unknown Device"} -

    - <%= if session.alerts_enabled do %> - - <.icon name="hero-bell" class="h-3 w-3" /> Alerts On - - <% else %> - - <.icon name="hero-bell-slash" class="h-3 w-3" /> Alerts Off - - <% end %> -
    -
    -

    {session.device_os} • {session.app_version}

    - - - -

    Last used {Calendar.strftime(session.last_used_at, "%B %d, %Y")}

    -
    -
    -
    - - -
    -
  • - <% end %> -
- -
- <.link - navigate={~p"/mobile/qr-login"} - class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400" - > - <.icon name="hero-qr-code" class="-ml-0.5 h-5 w-5" /> Add Device - -
- <% end %> + <% end %> +
-
+ <% end %> - -
-
-

- Security - Passkeys -

-

- Use your device's biometrics (Face ID, Touch ID, Windows Hello) or security keys to sign in. -

-
+ + <%= if @active_tab == "security" do %> + +
+
+

+ Passkeys +

+

+ Use your device's biometrics (Face ID, Touch ID, Windows Hello) or security keys to sign in. +

+
-
- <%= if !@can_register_passkey do %> -
-

- Please confirm your email address before registering a passkey. Check your inbox for the confirmation link. -

-
- <% end %> +
+ <%= if !@can_register_passkey do %> +
+

+ Please confirm your email address before registering a passkey. Check your inbox for the confirmation link. +

+
+ <% end %> + + <%= if Enum.empty?(@credentials) do %> +
+ <.icon name="hero-key" class="mx-auto h-12 w-12 text-gray-400" /> +

+ No passkeys +

+

+ Add a passkey to enable quick, secure sign-in with biometrics. +

+ <%= if @can_register_passkey do %> +
+ +
+ <% end %> +
+ <% else %> +
    + <%= for credential <- @credentials do %> +
  • +
    +
    +

    + {credential.name} +

    +
    +
    + <%= if credential.last_used_at do %> + Last used {Calendar.strftime(credential.last_used_at, "%B %d, %Y")} + <% else %> + Never used + <% end %> +
    +
    + <.link + href={~p"/users/credentials/#{credential.id}"} + method="delete" + data-confirm="Are you sure you want to delete this passkey?" + class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20" + > + Delete + +
  • + <% end %> +
- <%= if Enum.empty?(@credentials) do %> -
- <.icon name="hero-key" class="mx-auto h-12 w-12 text-gray-400" /> -

No passkeys

-

- Add a passkey to enable quick, secure sign-in with biometrics. -

<%= if @can_register_passkey do %> -
+
<% end %> -
- <% else %> -
    - <%= for credential <- @credentials do %> -
  • -
    -
    -

    - {credential.name} -

    -
    -
    - <%= if credential.last_used_at do %> - Last used {Calendar.strftime(credential.last_used_at, "%B %d, %Y")} - <% else %> - Never used - <% end %> -
    -
    - <.link - href={~p"/users/credentials/#{credential.id}"} - method="delete" - data-confirm="Are you sure you want to delete this passkey?" - class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20" - > - Delete - -
  • - <% end %> -
- - <%= if @can_register_passkey do %> -
- -
<% end %> - <% end %> +
-
+ <% end %>