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

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion lib/atomic/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,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
3 changes: 2 additions & 1 deletion lib/atomic_web/components/sidebar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule AtomicWeb.Components.Sidebar do
alias Phoenix.LiveView.JS
import AtomicWeb.Components.Icon
alias Atomic.Organizations
alias Atomic.Uploaders.ProfilePicture

attr :current_user, :map, required: true
attr :current_organization, :map, required: true
Expand Down Expand Up @@ -190,7 +191,7 @@ defmodule AtomicWeb.Components.Sidebar do

defp user_image(user) do
if user.profile_picture do
Uploaders.ProfilePicture.url({user, user.profile_picture}, :original)
ProfilePicture.url({user.profile_picture, user}, :original)
else
nil
end
Expand Down
65 changes: 56 additions & 9 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,56 @@ 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 != nil 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-2">
<.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" />
<.field field={f[:slug]} type="text" placeholder="User Name" 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 +60,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 +78,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 @@ -60,7 +107,7 @@ defmodule AtomicWeb.ProfileLive.FormComponent do
{:noreply,
socket
|> put_flash(:success, flash_text)
|> push_navigate(to: ~p"/profile/#{user_params["slug"]}")}
|> push_navigate(to: ~p"/profile/#{user_params["slug"] || user.slug}")}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
Expand All @@ -69,8 +116,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
76 changes: 62 additions & 14 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>
</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>
<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">
<!-- 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>
<% end %>
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


<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 @@ -32,11 +84,7 @@
<% end %>
</div>
</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>

<%= if @is_current_user do %>
<div class="w-24 flex justify-end">
<.button patch={~p"/profile/#{@user}/edit"}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule Atomic.Repo.Migrations.CreateUsersAuthTables do
add :slug, :citext
add :role, :string, null: false, default: "student"

add :socials, :map

add :hashed_password, :string, null: false

add :confirmed_at, :naive_datetime
Expand Down
Loading