Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/css/components/avatar.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
@apply size-24 text-4xl;
}

.atomic-avatar--xxl {
@apply size-28 text-4xl;
}

/* Avatar - colors */

.atomic-avatar--primary {
Expand Down
2 changes: 1 addition & 1 deletion lib/atomic/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
4 changes: 2 additions & 2 deletions lib/atomic_web/components/avatar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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."

Expand Down Expand Up @@ -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."

Expand Down
4 changes: 2 additions & 2 deletions lib/atomic_web/components/socials.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ defmodule AtomicWeb.Components.Socials do
assigns = assign(assigns, :socials_with_values, get_social_values(assigns.entity))

~H"""
<div class="grid grid-cols-2 gap-2 md:flex md:flex-row">
<div class="flex flex-wrap gap-2">
<%= for {social, icon, url_base, social_value} <- assigns.socials_with_values do %>
<%= if social_value do %>
<div class="flex flex-row items-center gap-x-2">
<img src={"/images/" <> 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}
</.link>
</div>
<% end %>
Expand Down
73 changes: 70 additions & 3 deletions lib/atomic_web/live/profile_live/show.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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
alias AtomicWeb.HomeLive.Components.FollowSuggestions.Suggestion

alias Atomic.Accounts
alias Atomic.Organizations
Expand All @@ -27,21 +28,87 @@ 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 =
Map.has_key?(socket.assigns, :current_user) and socket.assigns.current_user.id == user.id

organizations = Organizations.list_user_organizations(user.id)

memberships = Organizations.list_memberships(%{"user_id" => user.id}, [:organization])

{:noreply,
socket
|> assign(:page_title, user.name)
|> assign_page_metadata(:user_profile)
|> 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))}
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} ->
# Reloads memberships list after unfollowing a new one
memberships =
Organizations.list_memberships(%{"user_id" => socket.assigns.user.id}, [:organization])

{:noreply,
socket
|> assign(:memberships, memberships || [])
|> put_flash(:success, "Unfollowed " <> organization.name)}

{:error, _changeset} ->
{:noreply,
socket
|> put_flash(:error, "Failed to unfollow " <> organization.name)}
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

defp current_tab(_socket, _params), do: "following"
end
196 changes: 132 additions & 64 deletions lib/atomic_web/live/profile_live/show.html.heex
Original file line number Diff line number Diff line change
@@ -1,82 +1,150 @@
<div>
<div class="relative">
<div>

<!-- Banner -->
<div class="h-64 w-full">
<%= 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)} />
<% else %>
<.gradient class="h-64 w-full bg-center object-cover" seed={@user.id} />
<% end %>
</div>
<div class="relative px-4 pt-4">
<div class="flex items-start">
<div class="relative -mt-16 flex-shrink-0">
<div class="relative">
<%= 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} />
</:placeholder>
</.image_uploader>
<% else %>
<.avatar size={:xl} name={@user.name} type={:user} />
<% end %>
</div>

<!-- Avatar -->
<div class="-mt-16 flex px-8 md:-mt-14">
<div class="flex-shrink-0">
<div>
<%= 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} />
</:placeholder>
</.image_uploader>
<% else %>
<.avatar size={:xxl} name={@user.name} type={:user} />
<% end %>
</div>
<div class="flex-1 pl-6">
<h2 class="text-xl font-bold leading-7 text-zinc-900 sm:text-4xl">
{@user.name}
</h2>
<div class="mt-2">
<%= if length(@organizations) > 0 do %>
<div class="mt-2">
<%= for organization <- @organizations do %>
<p class="text-lg font-semibold text-zinc-600 md:text-md lg:text-sm">
{organization.name} - {Atomic.Organizations.get_role(@user.id, organization.id)}
</p>
<% end %>
</div>
<% else %>
<p class="py-2">{gettext("No organizations found.")}</p>
<% end %>
</div>
<%= if @user.socials do %>
<div class="mt-2">
<.socials entity={@user} />
</div>
</div>

<!-- User Info -->
<div class="flex w-full flex-col items-start justify-between px-8 py-4 md:flex-row">
<div class="flex flex-1 flex-col items-start gap-2 overflow-hidden">

<!-- Name-->
<h2 class="max-w-full truncate text-3xl font-bold leading-7 text-zinc-800 md:text-4xl">
{@user.name}
</h2>

<!-- Role -->
<div class="w-full overflow-hidden">
<%= if length(@organizations) > 0 do %>
<div>
<%= for organization <- @organizations do %>
<p class="truncate text-lg font-semibold text-zinc-600 lg:text-sm">
{organization.name} - {Atomic.Organizations.get_role(@user.id, organization.id)}
</p>
<% end %>
</div>
<% else %>
<p class="text-zinc-600">{gettext("No organizations found.")}</p>
<% end %>
<div class="mt-4 flex flex-col gap-8 md:flex-row">
<%= if @user.email do %>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-zinc-500">{gettext("Email")}</dt>
<dd class="mt-1 text-sm text-zinc-900">
<a class="text-blue-400" href={"mailto:#{@user.email}"}>
{@user.email}
</a>
</dd>
</div>
<% end %>
<%= if @user.phone_number do %>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-zinc-500">{gettext("Phone")}</dt>
<dd class="mt-1 text-sm text-zinc-900">
<a class="text-blue-400" href={"tel:#{@user.phone_number}"}>
{@user.phone_number}
</a>
</dd>
</div>
<% end %>
</div>
</div>

<!-- Socials -->
<%= if @user.socials do %>
<div class="mt-1 flex">
<.socials entity={@user} />
</div>
<% end %>
</div>

<!-- Edit Profile -->
<div class="mt-5 w-full flex-shrink-0 md:mt-0 md:w-fit">
<%= if @is_current_user do %>
<.button patch={~p"/profile/#{@user}/edit"} icon="hero-pencil-square" size={:md} full_width={true}>
{gettext("Edit Profile")}
</.button>
<% end %>
</div>
</div>
</div>
<div class="mt-8 flex w-full justify-end">
<%= if @is_current_user do %>
<div class="mr-6 flex w-24 justify-end md:mr-0">
<.button patch={~p"/profile/#{@user}/edit"}>
{gettext("Edit")}
</.button>

<!-- body -->

<div class="pt-8">
<div class="flex flex-col-reverse border-b border-zinc-200 xl:flex-row">
<div class="flex w-full items-center justify-between">
<.tabs class="px-4 sm:px-6 lg:px-8">
<.link patch="?tab=following" replace={false}>
<.tab active={@current_tab == "following"}>
{gettext("Following")}
</.tab>
</.link>
<.link patch="?tab=activity" replace={false}>
<.tab active={@current_tab == "activity"}>
{gettext("Activity")}
</.tab>
</.link>
<.link patch="?tab=about" replace={false}>
<.tab active={@current_tab == "about"}>
{gettext("About")}
</.tab>
</.link>
</.tabs>
</div>
<% end %>
</div>
</div>

<%= case @current_tab do %>
<% "following" -> %>
<section class="overflow-hidden px-8 py-6">
<div class="mx-auto max-w-5xl">
<p class="text-xl font-bold text-zinc-600">{gettext("Following Organizations:")}</p>

<div class="relative w-full">
<div class="hide-scrollbar min-h-24 flex flex-row gap-3 overflow-x-auto pt-4 pb-3">
<%= if Enum.any?(@memberships) do %>
<%= for membership <- @memberships do %>
<% org = membership.organization %>

<% is_viewer_following =
if @current_user do
Organizations.get_role(@current_user.id, org.id) == :follower
else
false
end %>

<div class="flex h-24 w-fit items-center rounded-lg border-2 border-zinc-200 px-4">
<.live_component id={org.id} module={Suggestion} organization={org} current_user={@current_user} is_following={is_viewer_following} />
</div>
<% end %>

<!-- Browse more organizations card -->
<div class="min-w-64 flex flex-shrink-0 flex-col items-center gap-2 rounded-lg border-2 border-dashed border-zinc-200 p-4">
<div>
<p class="text-zinc-500">{gettext("Browse more organizations")}</p>
</div>
<.button patch={~p"/organizations"} color={:white} size={:xs}>
{gettext("Browse Organizations")}
</.button>
</div>

<div class="pointer-events-none absolute top-0 -right-0 bottom-0 w-16 bg-gradient-to-l from-white to-transparent"></div>
<% else %>
<p class="font-light text-zinc-500">No memberships found...</p>
<% end %>
</div>
</div>
</div>
</section>
<% "activity" -> %>
<section class="px-8 py-6">
activity
</section>
<% "about" -> %>
<section class="px-8 py-6">
about
</section>
<% end %>
</div>
2 changes: 1 addition & 1 deletion lib/atomic_web/templates/layout/live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{render("_live_navbar.html", assigns)}
</div>
<!-- Central content -->
<div class="content-height w-full">
<div class="content-height w-full overflow-auto">
{@inner_content}
</div>
</div>
Expand Down
Loading