From df51731ae88124b0705bdf84317d3078ed8d98bb Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 16 Jul 2025 15:21:57 +0200 Subject: [PATCH 1/9] fix: added SetLocale to update LiveViews if language changes --- lib/mv_web/live/set_locale.ex | 13 +++++++++++++ lib/mv_web/router.ex | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 lib/mv_web/live/set_locale.ex diff --git a/lib/mv_web/live/set_locale.ex b/lib/mv_web/live/set_locale.ex new file mode 100644 index 0000000..bc8625d --- /dev/null +++ b/lib/mv_web/live/set_locale.ex @@ -0,0 +1,13 @@ +defmodule MvWeb.Live.SetLocale do + @moduledoc """ + The Module to make the locale available to all LiveViews + """ + import Phoenix.Component + import MvWeb.Gettext + + def on_mount(:default, _params, session, socket) do + locale = session["locale"] || "en" + Gettext.put_locale(locale) + {:cont, assign(socket, :locale, locale)} + end +end diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex index 75210b0..e58223e 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -46,7 +46,7 @@ defmodule MvWeb.Router do AshAuthentication-specific: We define that all routes can only be accessed when the user is signed in. """ ash_authentication_live_session :authentication_required, - on_mount: {MvWeb.LiveUserAuth, :live_user_required} do + on_mount: [{MvWeb.Live.SetLocale, :default}, {MvWeb.LiveUserAuth, :live_user_required}] do get "/", PageController, :home live "/members", MemberLive.Index, :index From e0fe001d34840d9e9275cbcdf636ea87ecc01989 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 16 Jul 2025 15:42:45 +0200 Subject: [PATCH 2/9] feature(memberslist): added columns to memberslist and added selection and sortable header --- lib/mv_web/components/layouts.ex | 6 +- lib/mv_web/components/table_components.ex | 44 +++++++ lib/mv_web/live/member_live/index.ex | 120 ++++++++++++-------- lib/mv_web/live/member_live/index.html.heex | 80 +++++++++++++ 4 files changed, 200 insertions(+), 50 deletions(-) create mode 100644 lib/mv_web/components/table_components.ex create mode 100644 lib/mv_web/live/member_live/index.html.heex diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index ba8ec67..f08c6ba 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -19,7 +19,7 @@ defmodule MvWeb.Layouts do

Content

- + """ attr :flash, :map, required: true, doc: "the map of flash messages" @@ -67,8 +67,8 @@ defmodule MvWeb.Layouts do -
-
+
+
{render_slot(@inner_block)}
diff --git a/lib/mv_web/components/table_components.ex b/lib/mv_web/components/table_components.ex new file mode 100644 index 0000000..ed94994 --- /dev/null +++ b/lib/mv_web/components/table_components.ex @@ -0,0 +1,44 @@ +defmodule MvWeb.TableComponents do + @moduledoc """ + TableComponents that can be used in tables as components (like a button for sorting, a filter...) + """ + use Phoenix.Component + import MvWeb.CoreComponents + use Gettext, backend: MvWeb.Gettext + + attr :field, :atom, required: true + attr :label, :string, required: true + attr :sort_field, :atom, default: nil + attr :sort_order, :atom, default: nil + + @doc """ + A sort button (with chevron icon) that can be used to sort a list of items + """ + def sort_button(assigns) do + ~H""" + + """ + end + + defp aria_sort(current_field, current_order, this_field) do + cond do + current_field != this_field -> "none" + current_order == :asc -> "ascending" + true -> "descending" + end + end +end diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 1cff898..38a6f93 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -1,58 +1,19 @@ defmodule MvWeb.MemberLive.Index do use MvWeb, :live_view - - @impl true - def render(assigns) do - ~H""" - - <.header> - {gettext("Listing Members")} - <:actions> - <.button variant="primary" navigate={~p"/members/new"}> - <.icon name="hero-plus" /> {gettext("New Member")} - - - - - <.table - id="members" - rows={@streams.members} - row_click={fn {_id, member} -> JS.navigate(~p"/members/#{member}") end} - > - - <:col :let={{_id, member}} label={gettext("First Name")}>{member.first_name} - <:col :let={{_id, member}} label={gettext("Last Name")}>{member.last_name} - <:col :let={{_id, member}} label={gettext("Email")}>{member.email} - <:col :let={{_id, member}} label={gettext("City")}>{member.city} - <:col :let={{_id, member}} label={gettext("Join Date")}>{member.join_date} - - <:action :let={{_id, member}}> -
- <.link navigate={~p"/members/#{member}"}>{gettext("Show")} -
- - <.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")} - - - <:action :let={{id, member}}> - <.link - phx-click={JS.push("delete", value: %{id: member.id}) |> hide("##{id}")} - data-confirm={gettext("Are you sure?")} - > - {gettext("Delete")} - - - -
- """ - end + import MvWeb.TableComponents @impl true def mount(_params, _session, socket) do + members = Ash.read!(Mv.Membership.Member) + sorted = Enum.sort_by(members, & &1.first_name) + {:ok, socket |> assign(:page_title, gettext("Listing Members")) - |> stream(:members, Ash.read!(Mv.Membership.Member))} + |> assign(:sort_field, :first_name) + |> assign(:sort_order, :asc) + |> assign(:members, sorted) + |> assign(:selected_members, [])} end @impl true @@ -62,4 +23,69 @@ defmodule MvWeb.MemberLive.Index do {:noreply, stream_delete(socket, :members, member)} end + + @doc """ + Selects one member in the list of members + """ + @impl true + def handle_event("select_member", %{"id" => id}, socket) do + selected = + if id in socket.assigns.selected_members do + List.delete(socket.assigns.selected_members, id) + else + [id | socket.assigns.selected_members] + end + + {:noreply, assign(socket, :selected_members, selected)} + end + + @doc """ + Sorts the list of members according to a field, when you click on the column header + """ + @impl true + def handle_event("sort", %{"field" => field_str}, socket) do + members = socket.assigns.members + field = String.to_existing_atom(field_str) + + new_order = + if socket.assigns.sort_field == field do + toggle_order(socket.assigns.sort_order) + else + :asc + end + + sorted_members = + members + |> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order)) + + {:noreply, + socket + |> assign(:sort_field, field) + |> assign(:sort_order, new_order) + |> assign(:members, sorted_members)} + end + + @doc """ + Selects all members in the list of members + """ + @impl true + def handle_event("select_all", _params, socket) do + members = socket.assigns.members + + all_ids = Enum.map(members, & &1.id) + + selected = + if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do + [] + else + all_ids + end + + {:noreply, assign(socket, :selected_members, selected)} + end + + defp toggle_order(:asc), do: :desc + defp toggle_order(:desc), do: :asc + defp sort_fun(:asc), do: &<=/2 + defp sort_fun(:desc), do: &>=/2 end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex new file mode 100644 index 0000000..b873805 --- /dev/null +++ b/lib/mv_web/live/member_live/index.html.heex @@ -0,0 +1,80 @@ + + <.header> + {gettext("Listing Members")} + <:actions> + <.button variant="primary" navigate={~p"/members/new"}> + <.icon name="hero-plus" /> {gettext("New Member")} + + + + + <.table + id="members" + rows={@members} + row_click={fn member -> JS.navigate(~p"/members/#{member}") end} + > + + + <:col :let={member} label={ + ~H""" + <.input + type="checkbox" + name="select_all" + phx-click="select_all" + checked={Enum.sort(@selected_members) == Enum.map(@members, & &1.id) |> Enum.sort()} + aria-label={gettext("Select all members")} + role="checkbox" + /> + """ + }> + <.input + type="checkbox" + name={member.id} + phx-click="select_member" + phx-value-id={member.id} + checked={member.id in @selected_members} + phx-capture-click + phx-stop-propagation + aria-label={gettext("Select member")} + role="checkbox" + /> + + <:col + :let={member} + label= { + sort_button(%{ + field: :first_name, + label: gettext("Name"), + sort_field: @sort_field, + sort_order: @sort_order + }) + } + > + {member.first_name} {member.last_name} + + <:col :let={member} label={gettext("Email")}>{member.email} + <:col :let={member} label={gettext("Street")}>{member.street} + <:col :let={member} label={gettext("House Number")}>{member.house_number} + <:col :let={member} label={gettext("Postal Code")}>{member.postal_code} + <:col :let={member} label={gettext("City")}>{member.city} + <:col :let={member} label={gettext("Phone Number")}>{member.phone_number} + <:col :let={member} label={gettext("Join Date")}>{member.join_date} + + <:action :let={member}> +
+ <.link navigate={~p"/members/#{member}"}>{gettext("Show")} +
+ + <.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")} + + + <:action :let={member}> + <.link + phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")} + data-confirm={gettext("Are you sure?")} + > + {gettext("Delete")} + + + +
From 1a66770bdb927fc11e13feba32482435d8de86d6 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 21 Jul 2025 11:45:29 +0200 Subject: [PATCH 3/9] feat (navbar): updated navbar with daisy UI component as demo --- lib/mv_web/components/layouts.ex | 39 ++--------------------- lib/mv_web/components/layouts/navbar.ex | 42 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 lib/mv_web/components/layouts/navbar.ex diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index f08c6ba..b109b25 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -8,6 +8,8 @@ defmodule MvWeb.Layouts do in regular views and live views. """ use MvWeb, :html + use Gettext, backend: MvWeb.Gettext + import MvWeb.Layouts.Navbar embed_templates "layouts/*" @@ -31,42 +33,7 @@ defmodule MvWeb.Layouts do def app(assigns) do ~H""" - - + <.navbar />
{render_slot(@inner_block)} diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex new file mode 100644 index 0000000..08184f3 --- /dev/null +++ b/lib/mv_web/components/layouts/navbar.ex @@ -0,0 +1,42 @@ +defmodule MvWeb.Layouts.Navbar do + @moduledoc """ + Navbar that is used in the rootlayout shown on every page + """ + use Phoenix.Component + use Gettext, backend: MvWeb.Gettext + + def navbar(assigns) do + ~H""" + + """ + end +end From e515f4672ecff21ace2bfa903032dd449ec46d4e Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 21 Jul 2025 11:45:56 +0200 Subject: [PATCH 4/9] formated files --- lib/mv_web/live/member_live/index.ex | 21 ++++++++------------- lib/mv_web/live/member_live/index.html.heex | 15 +++++++++------ mix.lock | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 38a6f93..a5ab3cb 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -24,9 +24,7 @@ defmodule MvWeb.MemberLive.Index do {:noreply, stream_delete(socket, :members, member)} end - @doc """ - Selects one member in the list of members - """ + # Selects one member in the list of members @impl true def handle_event("select_member", %{"id" => id}, socket) do selected = @@ -39,9 +37,7 @@ defmodule MvWeb.MemberLive.Index do {:noreply, assign(socket, :selected_members, selected)} end - @doc """ - Sorts the list of members according to a field, when you click on the column header - """ + # Sorts the list of members according to a field, when you click on the column header @impl true def handle_event("sort", %{"field" => field_str}, socket) do members = socket.assigns.members @@ -59,15 +55,14 @@ defmodule MvWeb.MemberLive.Index do |> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order)) {:noreply, - socket - |> assign(:sort_field, field) - |> assign(:sort_order, new_order) - |> assign(:members, sorted_members)} + socket + |> assign(:sort_field, field) + |> assign(:sort_order, new_order) + |> assign(:members, sorted_members)} end - @doc """ - Selects all members in the list of members - """ + # Selects all members in the list of members + @impl true def handle_event("select_all", _params, socket) do members = socket.assigns.members diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index b873805..6092bdb 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -13,10 +13,12 @@ rows={@members} row_click={fn member -> JS.navigate(~p"/members/#{member}") end} > - - - <:col :let={member} label={ - ~H""" + + + <:col + :let={member} + label={ + ~H""" <.input type="checkbox" name="select_all" @@ -26,7 +28,8 @@ role="checkbox" /> """ - }> + } + > <.input type="checkbox" name={member.id} @@ -41,7 +44,7 @@ <:col :let={member} - label= { + label={ sort_button(%{ field: :first_name, label: gettext("Name"), diff --git a/mix.lock b/mix.lock index e0392cb..c3b0bba 100644 --- a/mix.lock +++ b/mix.lock @@ -17,7 +17,7 @@ "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, "ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, From 6f5901986827fcd88ae0a087b80b312a8940f768 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 21 Jul 2025 11:46:48 +0200 Subject: [PATCH 5/9] chore; added translation for listing members --- priv/gettext/en/LC_MESSAGES/default.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 3317236..abad993 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -91,7 +91,7 @@ msgstr "" #: lib/mv_web/member_live/index.ex:88 #, elixir-autogen, elixir-format msgid "Listing Members" -msgstr "" +msgstr "Members" #: lib/mv_web/member_live/index.ex:11 #: lib/mv_web/member_live/index.ex:82 From bbf760c2b5a8dee4913284e33b012475cb67ffbc Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 16 Jul 2025 15:42:45 +0200 Subject: [PATCH 6/9] feature(memberslist): added columns to memberslist and added selection and sortable header --- lib/mv_web/components/layouts.ex | 6 +- lib/mv_web/components/table_components.ex | 44 +++++++ lib/mv_web/live/member_live/index.ex | 120 ++++++++++++-------- lib/mv_web/live/member_live/index.html.heex | 80 +++++++++++++ 4 files changed, 200 insertions(+), 50 deletions(-) create mode 100644 lib/mv_web/components/table_components.ex create mode 100644 lib/mv_web/live/member_live/index.html.heex diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index ba8ec67..f08c6ba 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -19,7 +19,7 @@ defmodule MvWeb.Layouts do

Content

- + """ attr :flash, :map, required: true, doc: "the map of flash messages" @@ -67,8 +67,8 @@ defmodule MvWeb.Layouts do
-
-
+
+
{render_slot(@inner_block)}
diff --git a/lib/mv_web/components/table_components.ex b/lib/mv_web/components/table_components.ex new file mode 100644 index 0000000..ed94994 --- /dev/null +++ b/lib/mv_web/components/table_components.ex @@ -0,0 +1,44 @@ +defmodule MvWeb.TableComponents do + @moduledoc """ + TableComponents that can be used in tables as components (like a button for sorting, a filter...) + """ + use Phoenix.Component + import MvWeb.CoreComponents + use Gettext, backend: MvWeb.Gettext + + attr :field, :atom, required: true + attr :label, :string, required: true + attr :sort_field, :atom, default: nil + attr :sort_order, :atom, default: nil + + @doc """ + A sort button (with chevron icon) that can be used to sort a list of items + """ + def sort_button(assigns) do + ~H""" + + """ + end + + defp aria_sort(current_field, current_order, this_field) do + cond do + current_field != this_field -> "none" + current_order == :asc -> "ascending" + true -> "descending" + end + end +end diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 1cff898..38a6f93 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -1,58 +1,19 @@ defmodule MvWeb.MemberLive.Index do use MvWeb, :live_view - - @impl true - def render(assigns) do - ~H""" - - <.header> - {gettext("Listing Members")} - <:actions> - <.button variant="primary" navigate={~p"/members/new"}> - <.icon name="hero-plus" /> {gettext("New Member")} - - - - - <.table - id="members" - rows={@streams.members} - row_click={fn {_id, member} -> JS.navigate(~p"/members/#{member}") end} - > - - <:col :let={{_id, member}} label={gettext("First Name")}>{member.first_name} - <:col :let={{_id, member}} label={gettext("Last Name")}>{member.last_name} - <:col :let={{_id, member}} label={gettext("Email")}>{member.email} - <:col :let={{_id, member}} label={gettext("City")}>{member.city} - <:col :let={{_id, member}} label={gettext("Join Date")}>{member.join_date} - - <:action :let={{_id, member}}> -
- <.link navigate={~p"/members/#{member}"}>{gettext("Show")} -
- - <.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")} - - - <:action :let={{id, member}}> - <.link - phx-click={JS.push("delete", value: %{id: member.id}) |> hide("##{id}")} - data-confirm={gettext("Are you sure?")} - > - {gettext("Delete")} - - - -
- """ - end + import MvWeb.TableComponents @impl true def mount(_params, _session, socket) do + members = Ash.read!(Mv.Membership.Member) + sorted = Enum.sort_by(members, & &1.first_name) + {:ok, socket |> assign(:page_title, gettext("Listing Members")) - |> stream(:members, Ash.read!(Mv.Membership.Member))} + |> assign(:sort_field, :first_name) + |> assign(:sort_order, :asc) + |> assign(:members, sorted) + |> assign(:selected_members, [])} end @impl true @@ -62,4 +23,69 @@ defmodule MvWeb.MemberLive.Index do {:noreply, stream_delete(socket, :members, member)} end + + @doc """ + Selects one member in the list of members + """ + @impl true + def handle_event("select_member", %{"id" => id}, socket) do + selected = + if id in socket.assigns.selected_members do + List.delete(socket.assigns.selected_members, id) + else + [id | socket.assigns.selected_members] + end + + {:noreply, assign(socket, :selected_members, selected)} + end + + @doc """ + Sorts the list of members according to a field, when you click on the column header + """ + @impl true + def handle_event("sort", %{"field" => field_str}, socket) do + members = socket.assigns.members + field = String.to_existing_atom(field_str) + + new_order = + if socket.assigns.sort_field == field do + toggle_order(socket.assigns.sort_order) + else + :asc + end + + sorted_members = + members + |> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order)) + + {:noreply, + socket + |> assign(:sort_field, field) + |> assign(:sort_order, new_order) + |> assign(:members, sorted_members)} + end + + @doc """ + Selects all members in the list of members + """ + @impl true + def handle_event("select_all", _params, socket) do + members = socket.assigns.members + + all_ids = Enum.map(members, & &1.id) + + selected = + if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do + [] + else + all_ids + end + + {:noreply, assign(socket, :selected_members, selected)} + end + + defp toggle_order(:asc), do: :desc + defp toggle_order(:desc), do: :asc + defp sort_fun(:asc), do: &<=/2 + defp sort_fun(:desc), do: &>=/2 end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex new file mode 100644 index 0000000..b873805 --- /dev/null +++ b/lib/mv_web/live/member_live/index.html.heex @@ -0,0 +1,80 @@ + + <.header> + {gettext("Listing Members")} + <:actions> + <.button variant="primary" navigate={~p"/members/new"}> + <.icon name="hero-plus" /> {gettext("New Member")} + + + + + <.table + id="members" + rows={@members} + row_click={fn member -> JS.navigate(~p"/members/#{member}") end} + > + + + <:col :let={member} label={ + ~H""" + <.input + type="checkbox" + name="select_all" + phx-click="select_all" + checked={Enum.sort(@selected_members) == Enum.map(@members, & &1.id) |> Enum.sort()} + aria-label={gettext("Select all members")} + role="checkbox" + /> + """ + }> + <.input + type="checkbox" + name={member.id} + phx-click="select_member" + phx-value-id={member.id} + checked={member.id in @selected_members} + phx-capture-click + phx-stop-propagation + aria-label={gettext("Select member")} + role="checkbox" + /> + + <:col + :let={member} + label= { + sort_button(%{ + field: :first_name, + label: gettext("Name"), + sort_field: @sort_field, + sort_order: @sort_order + }) + } + > + {member.first_name} {member.last_name} + + <:col :let={member} label={gettext("Email")}>{member.email} + <:col :let={member} label={gettext("Street")}>{member.street} + <:col :let={member} label={gettext("House Number")}>{member.house_number} + <:col :let={member} label={gettext("Postal Code")}>{member.postal_code} + <:col :let={member} label={gettext("City")}>{member.city} + <:col :let={member} label={gettext("Phone Number")}>{member.phone_number} + <:col :let={member} label={gettext("Join Date")}>{member.join_date} + + <:action :let={member}> +
+ <.link navigate={~p"/members/#{member}"}>{gettext("Show")} +
+ + <.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")} + + + <:action :let={member}> + <.link + phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")} + data-confirm={gettext("Are you sure?")} + > + {gettext("Delete")} + + + +
From f17f8fe74db5f411d56c02a3240db687c7c2543f Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 21 Jul 2025 11:45:29 +0200 Subject: [PATCH 7/9] feat (navbar): updated navbar with daisy UI component as demo --- lib/mv_web/components/layouts.ex | 39 ++--------------------- lib/mv_web/components/layouts/navbar.ex | 42 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 lib/mv_web/components/layouts/navbar.ex diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index f08c6ba..b109b25 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -8,6 +8,8 @@ defmodule MvWeb.Layouts do in regular views and live views. """ use MvWeb, :html + use Gettext, backend: MvWeb.Gettext + import MvWeb.Layouts.Navbar embed_templates "layouts/*" @@ -31,42 +33,7 @@ defmodule MvWeb.Layouts do def app(assigns) do ~H""" - - + <.navbar />
{render_slot(@inner_block)} diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex new file mode 100644 index 0000000..08184f3 --- /dev/null +++ b/lib/mv_web/components/layouts/navbar.ex @@ -0,0 +1,42 @@ +defmodule MvWeb.Layouts.Navbar do + @moduledoc """ + Navbar that is used in the rootlayout shown on every page + """ + use Phoenix.Component + use Gettext, backend: MvWeb.Gettext + + def navbar(assigns) do + ~H""" + + """ + end +end From f00434767cd6a00e3a3f36f30c78203f2461ac84 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 21 Jul 2025 11:45:56 +0200 Subject: [PATCH 8/9] formated files --- lib/mv_web/live/member_live/index.ex | 21 ++++++++------------- lib/mv_web/live/member_live/index.html.heex | 15 +++++++++------ mix.lock | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 38a6f93..a5ab3cb 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -24,9 +24,7 @@ defmodule MvWeb.MemberLive.Index do {:noreply, stream_delete(socket, :members, member)} end - @doc """ - Selects one member in the list of members - """ + # Selects one member in the list of members @impl true def handle_event("select_member", %{"id" => id}, socket) do selected = @@ -39,9 +37,7 @@ defmodule MvWeb.MemberLive.Index do {:noreply, assign(socket, :selected_members, selected)} end - @doc """ - Sorts the list of members according to a field, when you click on the column header - """ + # Sorts the list of members according to a field, when you click on the column header @impl true def handle_event("sort", %{"field" => field_str}, socket) do members = socket.assigns.members @@ -59,15 +55,14 @@ defmodule MvWeb.MemberLive.Index do |> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order)) {:noreply, - socket - |> assign(:sort_field, field) - |> assign(:sort_order, new_order) - |> assign(:members, sorted_members)} + socket + |> assign(:sort_field, field) + |> assign(:sort_order, new_order) + |> assign(:members, sorted_members)} end - @doc """ - Selects all members in the list of members - """ + # Selects all members in the list of members + @impl true def handle_event("select_all", _params, socket) do members = socket.assigns.members diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index b873805..6092bdb 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -13,10 +13,12 @@ rows={@members} row_click={fn member -> JS.navigate(~p"/members/#{member}") end} > - - - <:col :let={member} label={ - ~H""" + + + <:col + :let={member} + label={ + ~H""" <.input type="checkbox" name="select_all" @@ -26,7 +28,8 @@ role="checkbox" /> """ - }> + } + > <.input type="checkbox" name={member.id} @@ -41,7 +44,7 @@ <:col :let={member} - label= { + label={ sort_button(%{ field: :first_name, label: gettext("Name"), diff --git a/mix.lock b/mix.lock index e0392cb..c3b0bba 100644 --- a/mix.lock +++ b/mix.lock @@ -17,7 +17,7 @@ "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, "ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, From 89eac7d8ca61910833f75b158ed45b7dbf19f10b Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 21 Jul 2025 11:46:48 +0200 Subject: [PATCH 9/9] chore; added translation for listing members --- priv/gettext/en/LC_MESSAGES/default.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 3317236..abad993 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -91,7 +91,7 @@ msgstr "" #: lib/mv_web/member_live/index.ex:88 #, elixir-autogen, elixir-format msgid "Listing Members" -msgstr "" +msgstr "Members" #: lib/mv_web/member_live/index.ex:11 #: lib/mv_web/member_live/index.ex:82