From a47a060da89a5525f441e68050e6b8aaff5efa5e Mon Sep 17 00:00:00 2001 From: 11GG20 Date: Thu, 19 Jun 2025 12:38:36 +0100 Subject: [PATCH 1/3] feat: new profile layout --- assets/css/components/avatar.css | 4 + lib/atomic/organizations.ex | 2 +- lib/atomic_web/components/avatar.ex | 4 +- lib/atomic_web/components/socials.ex | 4 +- lib/atomic_web/live/profile_live/show.ex | 56 ++++- .../live/profile_live/show.html.heex | 202 +++++++++++++----- .../templates/layout/live.html.heex | 2 +- 7 files changed, 216 insertions(+), 58 deletions(-) diff --git a/assets/css/components/avatar.css b/assets/css/components/avatar.css index 455b343c4..0dc1c79e0 100644 --- a/assets/css/components/avatar.css +++ b/assets/css/components/avatar.css @@ -40,6 +40,10 @@ @apply size-24 text-4xl; } +.atomic-avatar--xxl { + @apply size-28 text-4xl; +} + /* Avatar - colors */ .atomic-avatar--primary { diff --git a/lib/atomic/organizations.ex b/lib/atomic/organizations.ex index e2c58e8ae..39afe7474 100644 --- a/lib/atomic/organizations.ex +++ b/lib/atomic/organizations.ex @@ -279,8 +279,8 @@ defmodule Atomic.Organizations do def list_memberships(%{"user_id" => user_id}, preloads) do Membership |> where([a], a.user_id == ^user_id) - |> Repo.preload(preloads) |> Repo.all() + |> Repo.preload(preloads) end @doc """ diff --git a/lib/atomic_web/components/avatar.ex b/lib/atomic_web/components/avatar.ex index 88e1ffb22..28c68e72b 100644 --- a/lib/atomic_web/components/avatar.ex +++ b/lib/atomic_web/components/avatar.ex @@ -16,7 +16,7 @@ defmodule AtomicWeb.Components.Avatar do doc: "The type of entity associated with the avatar." attr :size, :atom, - values: [:xs, :sm, :md, :lg, :xl], + values: [:xs, :sm, :md, :lg, :xl, :xxl], default: :md, doc: "The size of the avatar." @@ -69,7 +69,7 @@ defmodule AtomicWeb.Components.Avatar do doc: "The type of entity associated with the avatars." attr :size, :atom, - values: [:xs, :sm, :md, :lg, :xl], + values: [:xs, :sm, :md, :lg, :xl, :xxl], default: :md, doc: "The size of the avatars." diff --git a/lib/atomic_web/components/socials.ex b/lib/atomic_web/components/socials.ex index 07f4a1281..a0e8e4fb6 100644 --- a/lib/atomic_web/components/socials.ex +++ b/lib/atomic_web/components/socials.ex @@ -9,13 +9,13 @@ defmodule AtomicWeb.Components.Socials do assigns = assign(assigns, :socials_with_values, get_social_values(assigns.entity)) ~H""" -
+
<%= for {social, icon, url_base, social_value} <- assigns.socials_with_values do %> <%= if social_value do %>
icon} class="h-5 w-5" alt={Atom.to_string(social)} /> <.link class="capitalize text-blue-500" target="_blank" href={url_base <> social_value}> - {Atom.to_string(social)} + {social_value}
<% end %> diff --git a/lib/atomic_web/live/profile_live/show.ex b/lib/atomic_web/live/profile_live/show.ex index 3ea935d48..79b2dbe5a 100644 --- a/lib/atomic_web/live/profile_live/show.ex +++ b/lib/atomic_web/live/profile_live/show.ex @@ -1,7 +1,7 @@ defmodule AtomicWeb.ProfileLive.Show do use AtomicWeb, :live_view - import AtomicWeb.Components.{Button, Avatar, Gradient, Socials} + import AtomicWeb.Components.{Button, Tabs, Avatar, Gradient, Socials} import AtomicWeb.Components.ImageUploader import AtomicWeb.LiveHelpers @@ -27,7 +27,7 @@ defmodule AtomicWeb.ProfileLive.Show do end @impl true - def handle_params(%{"slug" => user_slug}, _, socket) do + def handle_params(%{"slug" => user_slug} = params, _, socket) do user = Accounts.get_user_by_slug(user_slug) is_current_user = @@ -35,6 +35,15 @@ defmodule AtomicWeb.ProfileLive.Show do organizations = Organizations.list_user_organizations(user.id) + memberships = Organizations.list_memberships(%{"user_id" => user.id}, [:organization]) + + is_following = + if Map.has_key?(socket.assigns, :current_user) do + Organizations.list_memberships(%{"user_id" => user.id}, [:organization]) != [] + else + false + end + {:noreply, socket |> assign(:page_title, user.name) @@ -42,6 +51,47 @@ defmodule AtomicWeb.ProfileLive.Show do |> assign(:current_page, :profile) |> assign(:user, user) |> assign(:organizations, organizations) - |> assign(:is_current_user, is_current_user)} + |> assign(:memberships, memberships) + |> assign(:is_current_user, is_current_user) + |> assign(:current_tab, current_tab(socket, params)) + |> assign(:is_following, is_following)} end + + @impl true + def handle_event("unfollow", %{"organization_id" => organization_id}, socket) do + membership = + Organizations.get_membership_by_user_id_and_organization_id!( + socket.assigns.current_user.id, + organization_id + ) + + organization = Organizations.get_organization!(organization_id) + + case Organizations.delete_membership(membership) do + {:ok, _organization} -> + # Reload memberships after successful unfollow + memberships = + Organizations.list_memberships(%{"user_id" => socket.assigns.user.id}, [:organization]) + + # Handle the case when memberships might be nil or empty + is_following = memberships != nil && Enum.any?(memberships) + + {:noreply, + socket + |> assign(:memberships, memberships || []) + |> assign(:is_following, is_following) + |> put_flash(:success, "Unfollowed " <> organization.name)} + + {:error, _changeset} -> + {:noreply, + socket + |> put_flash(:error, "Failed to unfollow " <> organization.name)} + end + end + + defp current_tab(_socket, params) when is_map_key(params, "tab") do + params["tab"] + end + + defp current_tab(_socket, _params), do: "following" end diff --git a/lib/atomic_web/live/profile_live/show.html.heex b/lib/atomic_web/live/profile_live/show.html.heex index a41eef204..232453d6a 100644 --- a/lib/atomic_web/live/profile_live/show.html.heex +++ b/lib/atomic_web/live/profile_live/show.html.heex @@ -1,5 +1,7 @@
-
+
+ +
<%= if @user.banner do %> <.image_uploader editable={false} id="banner-picture" class="h-64 w-full" image_class="h-[290px] w-full object-cover" upload={@uploads.banner} icon="hero-photo" memory_unit="MB" image={Uploaders.Banner.url({@user.banner, @user}, :original, signed: true)} /> @@ -7,43 +9,158 @@ <.gradient class="h-64 w-full bg-center object-cover" seed={@user.id} /> <% end %>
-
-
-
-
- <%= if @user.profile_picture do %> - <.image_uploader editable={false} id="profile-picture" class="aspect-square w-36 border-4 border-white" rounded upload={@uploads.profile_picture} icon="hero-user" memory_unit="GB" image={Uploaders.ProfilePicture.url({@user.profile_picture, @user}, :original, signed: true)}> - <:placeholder> - <.avatar size={:xl} name={@user.name} type={:user} /> - - - <% else %> - <.avatar size={:xl} name={@user.name} type={:user} /> - <% end %> -
+ + +
+
+
+ <%= if @user.profile_picture do %> + <.image_uploader editable={false} id="profile-picture" class="aspect-square w-36 border-4 border-white" rounded upload={@uploads.profile_picture} icon="hero-user" memory_unit="GB" image={Uploaders.ProfilePicture.url({@user.profile_picture, @user}, :original, signed: true)}> + <:placeholder> + <.avatar size={:xxl} name={@user.name} type={:user} /> + + + <% else %> + <.avatar size={:xxl} name={@user.name} type={:user} /> + <% end %>
-
-

- {@user.name} -

-
- <%= if length(@organizations) > 0 do %> -
- <%= for organization <- @organizations do %> -

- {organization.name} - {Atomic.Organizations.get_role(@user.id, organization.id)} -

- <% end %> -
- <% else %> -

{gettext("No organizations found.")}

- <% end %> -
- <%= if @user.socials do %> -
- <.socials entity={@user} /> +
+
+ + +
+
+ + +

+ {@user.name} +

+ + +
+ <%= if length(@organizations) > 0 do %> +
+ <%= for organization <- @organizations do %> +

+ {organization.name} - {Atomic.Organizations.get_role(@user.id, organization.id)} +

+ <% end %>
+ <% else %> +

{gettext("No organizations found.")}

<% end %> +
+ + + <%= if @user.socials do %> +
+ <.socials entity={@user} /> +
+ <% end %> +
+ + +
+ <%= if @is_current_user do %> + <.button patch={~p"/profile/#{@user}/edit"} icon="hero-pencil-square" size={:md} full_width={true}> + {gettext("Edit Profile")} + + <% end %> +
+
+
+ + + +
+
+
+ <.tabs class="px-4 sm:px-6 lg:px-8"> + <.link patch="?tab=following" replace={false}> + <.tab active={@current_tab == "following"}> + {gettext("Following")} + + + <.link patch="?tab=activity" replace={false}> + <.tab active={@current_tab == "activity"}> + {gettext("Activity")} + + + +
+
+
+ + <%= case @current_tab do %> + <% "following" -> %> +
+
+

{gettext("Following Organizations:")}

+ +
+
+ <%= if Enum.any?(@memberships) do %> + <%= for membership <- @memberships do %> + <% org = membership.organization %> + +
+ <.link navigate={~p"/organizations/#{org}"} class="flex min-w-0 flex-1 items-center justify-start gap-3"> +
+ <.avatar name={org.name} class="!h-10 !w-10 !text-lg" color={:light_zinc} size={:xs} type={:organization} src={Uploaders.Logo.url({org.logo, org}, :original)} /> +
+
+

+ {org.name} +

+

+ + {("@" <> org.name) |> String.downcase() |> String.replace(" ", "")} +

+
+ + +
+ <%= if @is_following do %> + <.button phx-value-organization_id={membership.organization.id} phx-click="unfollow" color={:white} size={:xs}> + <.icon name="hero-minus-solid" class="size-5 text-zinc-400" /> + Unfollow + + <% end %> +
+
+ + + <% end %> + + +
+
+

{gettext("Browse more organizations")}

+
+ <.button patch={~p"/organizations"} color={:white} size={:xs}> + {gettext("Browse Organizations")} + +
+ +
+ <% else %> +

No memberships found...

+ <% end %> +
+
+
+
+ <% "activity" -> %> +
+ activity +
+ <% end %> +
+ diff --git a/lib/atomic_web/templates/layout/live.html.heex b/lib/atomic_web/templates/layout/live.html.heex index 461ff37d1..2a5dac7cc 100644 --- a/lib/atomic_web/templates/layout/live.html.heex +++ b/lib/atomic_web/templates/layout/live.html.heex @@ -12,7 +12,7 @@ {render("_live_navbar.html", assigns)}
-
+
{@inner_content}
From 8040bae6c741681dd5a6fcfe6c5c2ec5259ffa31 Mon Sep 17 00:00:00 2001 From: 11GG20 Date: Sat, 28 Jun 2025 22:36:45 +0100 Subject: [PATCH 2/3] fix: improving overall code --- lib/atomic_web/live/profile_live/show.ex | 45 +++++++++++------ .../live/profile_live/show.html.heex | 50 +++++++------------ 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/lib/atomic_web/live/profile_live/show.ex b/lib/atomic_web/live/profile_live/show.ex index 79b2dbe5a..aedd7fe46 100644 --- a/lib/atomic_web/live/profile_live/show.ex +++ b/lib/atomic_web/live/profile_live/show.ex @@ -4,6 +4,7 @@ defmodule AtomicWeb.ProfileLive.Show do import AtomicWeb.Components.{Button, Tabs, Avatar, Gradient, Socials} import AtomicWeb.Components.ImageUploader import AtomicWeb.LiveHelpers + alias AtomicWeb.HomeLive.Components.FollowSuggestions.Suggestion alias Atomic.Accounts alias Atomic.Organizations @@ -37,13 +38,6 @@ defmodule AtomicWeb.ProfileLive.Show do memberships = Organizations.list_memberships(%{"user_id" => user.id}, [:organization]) - is_following = - if Map.has_key?(socket.assigns, :current_user) do - Organizations.list_memberships(%{"user_id" => user.id}, [:organization]) != [] - else - false - end - {:noreply, socket |> assign(:page_title, user.name) @@ -53,8 +47,7 @@ defmodule AtomicWeb.ProfileLive.Show do |> assign(:organizations, organizations) |> assign(:memberships, memberships) |> assign(:is_current_user, is_current_user) - |> assign(:current_tab, current_tab(socket, params)) - |> assign(:is_following, is_following)} + |> assign(:current_tab, current_tab(socket, params))} end @impl true @@ -69,17 +62,13 @@ defmodule AtomicWeb.ProfileLive.Show do case Organizations.delete_membership(membership) do {:ok, _organization} -> - # Reload memberships after successful unfollow + # Reloads memberships list after unfollowing a new one memberships = Organizations.list_memberships(%{"user_id" => socket.assigns.user.id}, [:organization]) - # Handle the case when memberships might be nil or empty - is_following = memberships != nil && Enum.any?(memberships) - {:noreply, socket |> assign(:memberships, memberships || []) - |> assign(:is_following, is_following) |> put_flash(:success, "Unfollowed " <> organization.name)} {:error, _changeset} -> @@ -89,6 +78,34 @@ defmodule AtomicWeb.ProfileLive.Show do end end + @impl true + def handle_event("follow", %{"organization_id" => organization_id}, socket) do + attrs = %{ + role: :follower, + user_id: socket.assigns.current_user.id, + created_by_id: socket.assigns.current_user.id, + organization_id: organization_id + } + + organization = Organizations.get_organization!(organization_id) + + case Organizations.create_membership(attrs) do + {:ok, _organization} -> + # Reloads memberships list after following a new one + memberships = + Organizations.list_memberships(%{"user_id" => socket.assigns.user.id}, [:organization]) + + {:noreply, + socket + |> assign(:memberships, memberships || []) + |> put_flash(:success, "Started following " <> organization.name) + |> push_patch(to: ~p"/profile/#{socket.assigns.user.slug}")} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + defp current_tab(_socket, params) when is_map_key(params, "tab") do params["tab"] end diff --git a/lib/atomic_web/live/profile_live/show.html.heex b/lib/atomic_web/live/profile_live/show.html.heex index 232453d6a..47dac764a 100644 --- a/lib/atomic_web/live/profile_live/show.html.heex +++ b/lib/atomic_web/live/profile_live/show.html.heex @@ -86,6 +86,11 @@ {gettext("Activity")} + <.link patch="?tab=about" replace={false}> + <.tab active={@current_tab == "about"}> + {gettext("About")} + +
@@ -98,42 +103,21 @@

{gettext("Following Organizations:")}

-
+
<%= if Enum.any?(@memberships) do %> <%= for membership <- @memberships do %> <% org = membership.organization %> -
- <.link navigate={~p"/organizations/#{org}"} class="flex min-w-0 flex-1 items-center justify-start gap-3"> -
- <.avatar name={org.name} class="!h-10 !w-10 !text-lg" color={:light_zinc} size={:xs} type={:organization} src={Uploaders.Logo.url({org.logo, org}, :original)} /> -
-
-

- {org.name} -

-

- - {("@" <> org.name) |> String.downcase() |> String.replace(" ", "")} -

-
- + <% is_viewer_following = + if @current_user do + Organizations.get_role(@current_user.id, org.id) == :follower + else + false + end %> -
- <%= if @is_following do %> - <.button phx-value-organization_id={membership.organization.id} phx-click="unfollow" color={:white} size={:xs}> - <.icon name="hero-minus-solid" class="size-5 text-zinc-400" /> - Unfollow - - <% end %> -
+
+ <.live_component id={org.id} module={Suggestion} organization={org} current_user={@current_user} is_following={is_viewer_following} />
- - <% end %> @@ -155,9 +139,13 @@
<% "activity" -> %> -
+
activity
+ <% "about" -> %> +
+ about +
<% end %>