Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: profile pages #528

Draft
wants to merge 20 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 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: 3 additions & 1 deletion lib/atomic/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ defmodule Atomic.Accounts do
def update_user_picture(%User{} = user, attrs \\ %{}) do
user
|> User.picture_changeset(attrs)
|> IO.inspect()
AfonsoMartins26 marked this conversation as resolved.
Show resolved Hide resolved
|> Repo.update()
end

Expand All @@ -491,10 +492,11 @@ defmodule Atomic.Accounts do
{:error, %Ecto.Changeset{}}

"""
def update_user(%User{} = user, attrs \\ %{}, _after_save \\ &{:ok, &1}) do
def update_user(%User{} = user, attrs \\ %{}, after_save \\ &{:ok, &1}) do
user
|> User.changeset(attrs)
|> Repo.update()
|> after_save(after_save)
end

@doc """
Expand Down
5 changes: 4 additions & 1 deletion lib/atomic/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule Atomic.Accounts.User do
alias Atomic.Accounts.Course
alias Atomic.Activities.Enrollment
alias Atomic.Organizations.{Collaborator, Membership, Organization}
alias Atomic.Socials

@required_fields ~w(email password)a
@optional_fields ~w(name slug role confirmed_at phone_number course_id current_organization_id)a
Expand Down Expand Up @@ -39,6 +40,8 @@ defmodule Atomic.Accounts.User do
has_many :enrollments, Enrollment
has_many :collaborators, Collaborator

embeds_one :socials, Socials, on_replace: :update

many_to_many :organizations, Organization, join_through: Membership

timestamps()
Expand Down Expand Up @@ -70,7 +73,6 @@ defmodule Atomic.Accounts.User do

def picture_changeset(user, attrs) do
user
|> cast(attrs, @required_fields ++ @optional_fields)
|> cast_attachments(attrs, [:profile_picture])
end

Expand All @@ -83,6 +85,7 @@ defmodule Atomic.Accounts.User do
|> validate_email()
|> validate_slug()
|> validate_phone_number()
|> cast_embed(:socials, with: &Socials.changeset/2)
end

defp validate_email(changeset) do
Expand Down
5 changes: 3 additions & 2 deletions lib/atomic_web/components/sidebar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
alias Phoenix.LiveView.JS
import AtomicWeb.Components.Icon
alias Atomic.Organizations
import AtomicWeb.Components.Avatar

Check warning on line 8 in lib/atomic_web/components/sidebar.ex

View workflow job for this annotation

GitHub Actions / Code Quality (26.x, 1.14.x)

unused import AtomicWeb.Components.Avatar

Check warning on line 8 in lib/atomic_web/components/sidebar.ex

View workflow job for this annotation

GitHub Actions / OTP 26.x / Elixir 1.14.x

unused import AtomicWeb.Components.Avatar

attr :current_user, :map, required: true
attr :current_organization, :map, required: true
Expand Down Expand Up @@ -190,9 +191,9 @@

defp user_image(user) do
if user.profile_picture do
Uploaders.ProfilePicture.url({user, user.profile_picture}, :original)
Atomic.Uploaders.ProfilePicture.url({user, user.profile_picture}, :original)
else
nil
""
end
end

Expand Down
62 changes: 54 additions & 8 deletions lib/atomic_web/live/profile_live/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,55 @@ defmodule AtomicWeb.ProfileLive.FormComponent do
use AtomicWeb, :live_component

alias Atomic.Accounts

@extensions_whitelist ~w(.jpg .jpeg .gif .png)
alias AtomicWeb.Components.ImageUploader
import AtomicWeb.Components.Forms
import AtomicWeb.Components.{Button, Avatar}

@impl true
def mount(socket) do
{:ok,
socket
|> allow_upload(:picture, accept: @extensions_whitelist, max_entries: 1)}
def render(assigns) do
~H"""
<div class="px-4 pt-4">
<.form :let={f} for={@changeset} id="profile-form" phx-target={@myself} phx-change="validate" phx-submit="save">
<!-- Grid layout for profile picture, name, phone number, email, and social media fields -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Section for profile picture upload -->
<div class="flex flex-col items-center pr-4">
<%= if @user.profile_picture != NULL do %>
<%= label(f, :name, "Profile Picture", class: "mt-3 mb-1 text-sm font-medium text-gray-700") %>
<div class="mb-4 border-4">
<.avatar name={@user.name} color={:light_gray} class="h-36 w-36 rounded-full border-4 border-white text-4xl" type={:user} src={Uploaders.ProfilePicture.url({@user.profile_picture, @user}, :original)} />
</div>
<.live_component module={ImageUploader} id="uploader-profile-picture" uploads={@uploads} target={@myself} />
<% else %>
<%= label(f, :name, "Profile Picture", class: "mt-3 mb-1 text-sm font-medium text-gray-700") %>
<.live_component module={ImageUploader} id="uploader-profile-picture" uploads={@uploads} target={@myself} />
<% end %>
</div>
<div class="flex flex-col gap-6">
<!-- Name, phone number, email fields -->
<div class="grid grid-cols-1 gap-4">
<.field field={f[:name]} type="text" placeholder="Name" class="w-full" />
<.field field={f[:phone_number]} type="text" placeholder="Phone Number" class="w-full" />
<.field field={f[:email]} type="email" placeholder="Email" class="w-full" />
</div>
<!-- Social media fields positioned below name, phone, and email -->
<div class="grid w-full gap-x-4 gap-y-4 sm:grid-cols-1 md:grid-cols-4">
<.inputs_for :let={socials_form} field={f[:socials]}>
<.field field={socials_form[:instagram]} type="text" placeholder="Instagram" class="w-full" />
<.field field={socials_form[:facebook]} type="text" placeholder="Facebook" class="w-full" />
<.field field={socials_form[:x]} type="text" placeholder="X" class="w-full" />
<.field field={socials_form[:tiktok]} type="text" placeholder="TikTok" class="w-full" />
</.inputs_for>
</div>
</div>
</div>
<!-- Submit button -->
<div class="mt-8 flex w-full justify-end">
<.button size={:md} color={:white} icon="hero-cube">Save</.button>
</div>
</.form>
</div>
"""
end

@impl true
Expand All @@ -18,6 +59,10 @@ defmodule AtomicWeb.ProfileLive.FormComponent do

{:ok,
socket
|> allow_upload(:image,
accept: Uploaders.ProfilePicture.extension_whitelist(),
max_entries: 1
)
|> assign(assigns)
|> assign(:changeset, changeset)}
end
Expand All @@ -32,6 +77,7 @@ defmodule AtomicWeb.ProfileLive.FormComponent do
{:noreply, assign(socket, :changeset, changeset)}
end

@impl true
def handle_event("save", %{"user" => user_params}, socket) do
user = socket.assigns.user

Expand Down Expand Up @@ -69,8 +115,8 @@ defmodule AtomicWeb.ProfileLive.FormComponent do

defp consume_image_data(socket, user) do
consume_uploaded_entries(socket, :image, fn %{path: path}, entry ->
Accounts.update_user(user, %{
"image" => %Plug.Upload{
Accounts.update_user_picture(user, %{
"profile_picture" => %Plug.Upload{
content_type: entry.client_type,
filename: entry.client_name,
path: path
Expand Down
86 changes: 0 additions & 86 deletions lib/atomic_web/live/profile_live/form_component.html.heex

This file was deleted.

3 changes: 1 addition & 2 deletions lib/atomic_web/live/profile_live/show.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
defmodule AtomicWeb.ProfileLive.Show do
use AtomicWeb, :live_view

import AtomicWeb.Components.Button
import AtomicWeb.Components.Avatar
import AtomicWeb.Components.{Button, Avatar, Gradient}

alias Atomic.Accounts
alias Atomic.Organizations
Expand Down
81 changes: 70 additions & 11 deletions lib/atomic_web/live/profile_live/show.html.heex
Original file line number Diff line number Diff line change
@@ -1,14 +1,66 @@
<div>
<div class="pt-4 px-4">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1 space-y-2">
<div class="flex flex-row">
<h2 class="text-xl font-bold leading-7 text-zinc-900 sm:text-4xl">
<%= @user.name %>
</h2>
<div class="relative">
<div class="h-64 w-full border-b-2 bg-cover">
<.gradient class="h-64 w-full bg-cover bg-center" seed={@user.id} />
</div>
<!-- Profile Info Container -->
<div class="relative px-4 pt-4">
<div class="flex items-start">
<!-- Profile Picture -->
<div class="relative -mt-16 flex-shrink-0">
<div class="relative">
<.avatar name={@user.name} color={:light_gray} class="h-36 w-36 text-4xl rounded-full border-4 border-white" type={:user} src={Uploaders.ProfilePicture.url({@user.profile_picture, @user}, :original)} />
</div>
<p class="text-zinc-500">@<%= @user.slug %></p>
<div class="grid grid-cols-1 gap-4 py-6 mb-2 sm:grid-cols-2 lg:grid-cols-3">
</div>
<div class="flex-1 pl-6">
<!-- User Info -->
<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">No organizations found.</p>
<% end %>
</div>
<!-- Social Media Links -->
<%= if @user.socials do %>
<div class="mt-4 flex gap-4">
<%= if @user.socials.tiktok do %>
<div class="flex flex-row items-center gap-x-1">
<img src="/images/tiktok.svg" class="h-5 w-5" alt="TikTok" />
<.link class="text-blue-500" target="_blank" href={"https://tiktok.com/" <> @user.socials.tiktok}>Tik Tok</.link>
</div>
<% end %>
<%= if @user.socials.instagram do %>
<div class="flex flex-row items-center gap-x-1">
<img src="/images/instagram.svg" class="h-5 w-5" alt="Instagram" />
<.link class="text-blue-500" target="_blank" href={"https://instagram.com/" <> @user.socials.instagram}>Instagram</.link>
</div>
<% end %>
<%= if @user.socials.facebook do %>
<div class="flex flex-row items-center gap-x-1">
<img src="/images/facebook.svg" class="h-5 w-5" alt="Facebook" />
<.link class="text-blue-500" target="_blank" href={"https://facebook.com/" <> @user.socials.facebook}>Facebook</.link>
</div>
<% end %>
<%= if @user.socials.x do %>
<div class="flex flex-row items-center gap-x-1">
<img src="/images/x.svg" class="h-5 w-5" alt="X" />
<.link class="text-blue-500" target="_blank" href={"https://x.com/" <> @user.socials.x}>X</.link>
</div>
<% end %>
</div>
Comment on lines +33 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is being used in multiple places please create a component

<% end %>

<div class="fllex-row mt-4 flex gap-8">
<%= if @user.email do %>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Email</dt>
Expand All @@ -31,8 +83,15 @@
</div>
<% end %>
</div>
<!-- Edit Button for Current User -->
<%= if @is_current_user do %>
<div class="mt-4 flex justify-start">
<.button patch={Routes.profile_edit_path(@socket, :edit, @user)}>

Check warning on line 89 in lib/atomic_web/live/profile_live/show.html.heex

View workflow job for this annotation

GitHub Actions / Code Quality (26.x, 1.14.x)

Routes.profile_edit_path/3 is undefined (module Routes is not available or is yet to be defined)

Check warning on line 89 in lib/atomic_web/live/profile_live/show.html.heex

View workflow job for this annotation

GitHub Actions / OTP 26.x / Elixir 1.14.x

Routes.profile_edit_path/3 is undefined (module Routes is not available or is yet to be defined)
<%= gettext("Edit") %>
</.button>
</div>
<% end %>
</div>
<.avatar class="sm:w-44 sm:h-44 sm:text-6xl" name={@user.name} size={:xl} color={:light_gray} />
</div>
<!-- Divider -->
<div class="py-6 mb-2 border-b border-zinc-200"></div>
Expand Down
Loading