From 7d2cca942260473b0f51e4cdab640e54a6b15883 Mon Sep 17 00:00:00 2001 From: lourw <56288712+lourw@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:06:39 -0700 Subject: [PATCH 1/2] feat(groups): ui page revamp for groups --- .../components/layouts/app.html.heex | 2 +- lib/split_app_web/live/group_live/index.ex | 5 + .../live/group_live/index.html.heex | 129 +++++++----- .../live/group_live/show.html.heex | 194 ++++++++++++++---- 4 files changed, 243 insertions(+), 87 deletions(-) diff --git a/lib/split_app_web/components/layouts/app.html.heex b/lib/split_app_web/components/layouts/app.html.heex index 31c274e..de563e2 100644 --- a/lib/split_app_web/components/layouts/app.html.heex +++ b/lib/split_app_web/components/layouts/app.html.heex @@ -24,7 +24,7 @@ <.flash_group flash={@flash} /> -
+
{@inner_content}
diff --git a/lib/split_app_web/live/group_live/index.ex b/lib/split_app_web/live/group_live/index.ex index df64658..6136ed6 100644 --- a/lib/split_app_web/live/group_live/index.ex +++ b/lib/split_app_web/live/group_live/index.ex @@ -55,6 +55,11 @@ defmodule SplitAppWeb.GroupLive.Index do {:noreply, put_flash(socket, type, message)} end + @impl true + def handle_event("edit_group", %{"id" => id}, socket) do + {:noreply, push_patch(socket, to: ~p"/groups/#{id}/edit")} + end + @impl true def handle_event("delete", %{"id" => id}, socket) do user = socket.assigns.current_user diff --git a/lib/split_app_web/live/group_live/index.html.heex b/lib/split_app_web/live/group_live/index.html.heex index dfbc2cd..f4f4119 100644 --- a/lib/split_app_web/live/group_live/index.html.heex +++ b/lib/split_app_web/live/group_live/index.html.heex @@ -1,4 +1,4 @@ -
+
<.header class="mb-8"> Your Groups <:subtitle>Manage your expense sharing groups @@ -11,62 +11,91 @@ -
-
    -
  • +
    + -
    -
    -
    -

    - {group.name} -

    -
    - - {get_user_count(group)} {if get_user_count(group) == 1, - do: "member", - else: "members"} + + +
    +

    No groups yet

    +

    + Create your first group to start splitting expenses with friends. +

    + <.link patch={~p"/groups/new"}> + <.button>New Group + +
    + +
    0} class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> +
    + <.link navigate={~p"/groups/#{group}"} class="block"> +
    +
    + + <.icon name="hero-user-group" class="h-6 w-6" /> + +
    +
    +

    + {group.name} +

    +

    + {if group.description && String.length(group.description) > 0, + do: group.description, + else: "No description provided"} +

    +
    +
    + <.icon name="hero-users" class="w-4 h-4 mr-1" /> + + {get_user_count(group)} {if get_user_count(group) == 1, do: "member", else: "members"}
    -
    -
    -

    - {if group.description && String.length(group.description) > 0, - do: group.description, - else: "No description"} -

    -
    - {format_date(group.inserted_at)} - <.link - patch={~p"/groups/#{group}/edit"} - class="text-indigo-600 hover:text-indigo-900" - > - Edit - - <.link - phx-click={JS.push("delete", value: %{id: group.id})} - data-confirm="Are you sure you want to delete this group? This will remove all associated data." - class="text-red-600 hover:text-red-900" - > - Delete - +
    + {format_date(group.inserted_at)}
    + +
    +
    + <.link + patch={~p"/groups/#{group}/edit"} + class="p-1 text-indigo-600 hover:text-indigo-900 bg-white rounded shadow-sm border border-gray-200" + phx-click="edit_group" + phx-value-id={group.id} + > + <.icon name="hero-pencil" class="w-4 h-4" /> + + +
    +
    -
  • -
- -
-

No groups yet

-

- Create your first group to start splitting expenses with friends. -

- <.link patch={~p"/groups/new"}> - <.button>New Group
diff --git a/lib/split_app_web/live/group_live/show.html.heex b/lib/split_app_web/live/group_live/show.html.heex index bddd878..a6124ff 100644 --- a/lib/split_app_web/live/group_live/show.html.heex +++ b/lib/split_app_web/live/group_live/show.html.heex @@ -1,48 +1,170 @@ -
+
- <.back navigate={~p"/groups"}>Back to groups + <.link + navigate={~p"/groups"} + class="text-sm font-semibold leading-6 text-gray-900 hover:text-gray-700 flex items-center gap-2" + > + <.icon name="hero-arrow-left" class="h-4 w-4" /> + Back to groups +
- <.header class="mb-8"> - {@group.name} - <:subtitle>Group details and members - <:actions> - <.link patch={~p"/groups/#{@group}/show/edit"} phx-click={JS.push_focus()}> - <.button class="flex items-center gap-2"> - <.icon name="hero-pencil-square" class="w-4 h-4" /> Edit - - - - +
+
+
+ + <.icon name="hero-user-group" class="h-8 w-8" /> + +
+

{@group.name}

+

+ {if @group.description && String.length(@group.description) > 0, + do: @group.description, + else: "No description provided"} +

+
+
+
+ <.link patch={~p"/groups/#{@group}/show/edit"} phx-click={JS.push_focus()}> + <.button class="flex items-center gap-2"> + <.icon name="hero-pencil-square" class="w-4 h-4" /> Edit Group + + +
+
+
-
-
-

Group Information

+ +
+
+
+
+
+
+ <.icon name="hero-users" class="w-5 h-5 text-blue-600" /> +
+
+
+
+
Members
+
+ {get_user_count(@group)} +
+
+
+
+
-
-
-
Name
-
{@group.name}
+ +
+
+
+
+
+ <.icon name="hero-banknotes" class="w-5 h-5 text-green-600" /> +
+
+
+
+
Total Expenses
+
$0.00
+
+
+
-
-
Description
-
- {if @group.description && String.length(@group.description) > 0, - do: @group.description, - else: "No description"} -
+
+ +
+
+
+
+
+ <.icon name="hero-receipt-percent" class="w-5 h-5 text-yellow-600" /> +
+
+
+
+
Your Share
+
$0.00
+
+
+
-
-
Members
-
- {get_user_count(@group)} {if get_user_count(@group) == 1, do: "member", else: "members"} -
+
+ +
+
+
+
+
+ <.icon name="hero-calendar-days" class="w-5 h-5 text-purple-600" /> +
+
+
+
+
Created
+
{format_date(@group.inserted_at)}
+
+
+
-
-
Created
-
{format_date(@group.inserted_at)}
+
+
+ + +
+ +
+
+
+

+ <.icon name="hero-users" class="w-5 h-5 mr-2 text-gray-500" /> + Group Members ({get_user_count(@group)}) +

+ +
-
+
+
+
+ <.icon name="hero-user-plus" class="w-full h-full" /> +
+

No members yet

+

Invite people to join this group.

+
+ +
+
+
0} class="text-center py-4"> +

Member management coming soon...

+
+
+
+ + +
+
+

+ <.icon name="hero-clock" class="w-5 h-5 mr-2 text-gray-500" /> + Recent Activity +

+
+
+
+
+ <.icon name="hero-document-text" class="w-full h-full" /> +
+

No activity yet

+

Activity will appear here as members add expenses.

+
+
+
From b2488b9f0cb65830fcdaaf1b84077d3ac46c0d68 Mon Sep 17 00:00:00 2001 From: lourw <56288712+lourw@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:22:44 -0700 Subject: [PATCH 2/2] chore(groups): add tests and formatting --- lib/split_app_web/live/dashboard_live.ex | 11 +--- lib/split_app_web/live/expense_live/index.ex | 2 +- .../live/group_live/index.html.heex | 9 ++- .../live/group_live/show.html.heex | 32 +++++------ .../live/dashboard_live_test.exs | 56 ++++++++++--------- test/split_app_web/live/group_live_test.exs | 6 +- 6 files changed, 57 insertions(+), 59 deletions(-) diff --git a/lib/split_app_web/live/dashboard_live.ex b/lib/split_app_web/live/dashboard_live.ex index 350bf2d..e3a4ab1 100644 --- a/lib/split_app_web/live/dashboard_live.ex +++ b/lib/split_app_web/live/dashboard_live.ex @@ -73,7 +73,7 @@ defmodule SplitAppWeb.DashboardLive do def handle_info({SplitAppWeb.ExpenseLive.FormComponent, {:saved, _expense}}, socket) do user = socket.assigns.current_user recent_expenses = Expenses.list_expenses_by_user(user.id) |> Enum.take(5) - + {:noreply, socket |> assign(:recent_expenses, recent_expenses) @@ -86,7 +86,7 @@ defmodule SplitAppWeb.DashboardLive do def handle_info({SplitAppWeb.GroupLive.FormComponent, {:saved, _group}}, socket) do user = socket.assigns.current_user groups = Groups.list_user_groups(user) - + {:noreply, socket |> assign(:groups, groups) @@ -331,12 +331,7 @@ defmodule SplitAppWeb.DashboardLive do /> - <.modal - :if={@show_group_modal} - id="group-modal" - show - on_cancel={JS.push("close_group_modal")} - > + <.modal :if={@show_group_modal} id="group-modal" show on_cancel={JS.push("close_group_modal")}> <.live_component module={SplitAppWeb.GroupLive.FormComponent} id={:new} diff --git a/lib/split_app_web/live/expense_live/index.ex b/lib/split_app_web/live/expense_live/index.ex index d54f5c9..ec9161f 100644 --- a/lib/split_app_web/live/expense_live/index.ex +++ b/lib/split_app_web/live/expense_live/index.ex @@ -47,7 +47,7 @@ defmodule SplitAppWeb.ExpenseLive.Index do def handle_info({SplitAppWeb.ExpenseLive.FormComponent, {:saved, _expense}}, socket) do current_user = socket.assigns.current_user expenses = if current_user, do: Expenses.list_expenses_by_user(current_user.id), else: [] - + {:noreply, socket |> stream(:expenses, expenses, reset: true) diff --git a/lib/split_app_web/live/group_live/index.html.heex b/lib/split_app_web/live/group_live/index.html.heex index f4f4119..ba5c3da 100644 --- a/lib/split_app_web/live/group_live/index.html.heex +++ b/lib/split_app_web/live/group_live/index.html.heex @@ -37,10 +37,7 @@
0} class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> -
+
<.link navigate={~p"/groups/#{group}"} class="block">
@@ -61,7 +58,9 @@
<.icon name="hero-users" class="w-4 h-4 mr-1" /> - {get_user_count(group)} {if get_user_count(group) == 1, do: "member", else: "members"} + {get_user_count(group)} {if get_user_count(group) == 1, + do: "member", + else: "members"}
diff --git a/lib/split_app_web/live/group_live/show.html.heex b/lib/split_app_web/live/group_live/show.html.heex index a6124ff..098178f 100644 --- a/lib/split_app_web/live/group_live/show.html.heex +++ b/lib/split_app_web/live/group_live/show.html.heex @@ -4,8 +4,7 @@ navigate={~p"/groups"} class="text-sm font-semibold leading-6 text-gray-900 hover:text-gray-700 flex items-center gap-2" > - <.icon name="hero-arrow-left" class="h-4 w-4" /> - Back to groups + <.icon name="hero-arrow-left" class="h-4 w-4" /> Back to groups
@@ -33,8 +32,8 @@
- - + +
@@ -103,15 +102,17 @@
Created
-
{format_date(@group.inserted_at)}
+
+ {format_date(@group.inserted_at)} +
- - + +
@@ -122,8 +123,7 @@ Group Members ({get_user_count(@group)})
@@ -136,8 +136,7 @@

Invite people to join this group.

@@ -146,13 +145,12 @@
- - + +

- <.icon name="hero-clock" class="w-5 h-5 mr-2 text-gray-500" /> - Recent Activity + <.icon name="hero-clock" class="w-5 h-5 mr-2 text-gray-500" /> Recent Activity

@@ -161,7 +159,9 @@ <.icon name="hero-document-text" class="w-full h-full" />

No activity yet

-

Activity will appear here as members add expenses.

+

+ Activity will appear here as members add expenses. +

diff --git a/test/split_app_web/live/dashboard_live_test.exs b/test/split_app_web/live/dashboard_live_test.exs index 326cd6d..3fcd41e 100644 --- a/test/split_app_web/live/dashboard_live_test.exs +++ b/test/split_app_web/live/dashboard_live_test.exs @@ -56,18 +56,18 @@ defmodule SplitAppWeb.DashboardLiveTest do # Wait for the update :timer.sleep(100) - + # Verify we're still on the dashboard (not redirected) html = render(live) assert html =~ "Welcome" - + # Verify expense was created and shows in recent expenses assert html =~ "Dashboard Expense" assert html =~ "Test expense from dashboard" - + # Verify modal is closed refute has_element?(live, "#expense-modal") - + # Verify flash message assert html =~ "Expense created successfully" end @@ -89,7 +89,7 @@ defmodule SplitAppWeb.DashboardLiveTest do # Verify modal is closed refute has_element?(live, "#expense-modal") - + # Verify we're still on the dashboard assert render(live) =~ "Welcome" assert render(live) =~ "Recent Expenses" @@ -97,17 +97,19 @@ defmodule SplitAppWeb.DashboardLiveTest do test "displays existing expenses in recent expenses section", %{conn: conn, user: user} do # Create some expenses - expense1 = expense_fixture(%{ - created_by_id: user.id, - title: "First Expense", - description: "First Description" - }) - - expense2 = expense_fixture(%{ - created_by_id: user.id, - title: "Second Expense", - description: "Second Description" - }) + expense1 = + expense_fixture(%{ + created_by_id: user.id, + title: "First Expense", + description: "First Description" + }) + + expense2 = + expense_fixture(%{ + created_by_id: user.id, + title: "Second Expense", + description: "Second Description" + }) {:ok, _live, html} = live(conn, ~p"/dashboard") @@ -185,26 +187,28 @@ defmodule SplitAppWeb.DashboardLiveTest do # Fill and submit form assert live - |> form("#group-form", group: %{ - name: "Dashboard Test Group", - description: "Created from dashboard" - }) + |> form("#group-form", + group: %{ + name: "Dashboard Test Group", + description: "Created from dashboard" + } + ) |> render_submit() # Wait for the update :timer.sleep(100) - + # Verify we're still on the dashboard (not redirected) html = render(live) assert html =~ "Welcome" - + # Verify group was created and shows in groups section assert html =~ "Dashboard Test Group" assert html =~ "Created from dashboard" - + # Verify modal is closed refute has_element?(live, "#group-modal") - + # Verify flash message assert html =~ "Group created successfully" end @@ -225,7 +229,7 @@ defmodule SplitAppWeb.DashboardLiveTest do # Verify modal is closed refute has_element?(live, "#group-modal") - + # Verify we're still on the dashboard assert render(live) =~ "Welcome" assert render(live) =~ "Your Groups" @@ -245,4 +249,4 @@ defmodule SplitAppWeb.DashboardLiveTest do |> render_change() =~ "can't be blank" end end -end \ No newline at end of file +end diff --git a/test/split_app_web/live/group_live_test.exs b/test/split_app_web/live/group_live_test.exs index 0038909..17a8997 100644 --- a/test/split_app_web/live/group_live_test.exs +++ b/test/split_app_web/live/group_live_test.exs @@ -49,7 +49,7 @@ defmodule SplitAppWeb.GroupLiveTest do test "updates group in listing", %{conn: conn, group: group} do {:ok, index_live, _html} = live(conn, ~p"/groups") - assert index_live |> element("a", "Edit") |> render_click() =~ + assert index_live |> element("a[phx-click='edit_group']") |> render_click() =~ "Edit Group" assert_patch(index_live, ~p"/groups/#{group}/edit") @@ -72,8 +72,8 @@ defmodule SplitAppWeb.GroupLiveTest do test "deletes group in listing", %{conn: conn, group: group} do {:ok, index_live, _html} = live(conn, ~p"/groups") - assert index_live |> element("a", "Delete") |> render_click() - refute has_element?(index_live, "li", group.name) + assert index_live |> element("button[phx-click='delete']") |> render_click() + refute has_element?(index_live, "div", group.name) end end