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 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
Binary file added app/assets/images/partners/cafedoluis.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/partners/texasburger.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
617 changes: 617 additions & 0 deletions app/assets/images/partners/thetraditionalgreatpizza.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/partners/untoldstories.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/alexandre-gomes.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/alexandre-neves.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/beatriz-rodrigues.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/daniel-pereira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/enzo-vieira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/filipe-felicio.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/gabriela-prata.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/gerson-junior.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/goncalo-costa.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/goncalo-rodrigues.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/gustavo-pereira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/henrique-pereira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/ines-marinho.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/jessica-fernandes.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/jose-ferreira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/julio-pinto.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/lara-pereira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/leonardo-freitas.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/luis-araujo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/marco-pereira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/martim-ferreira.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/team/matilde-bravo.jpg
Binary file added app/assets/images/team/miguel-gramoso.jpg
Binary file added app/assets/images/team/pedro-antonio.jpg
Binary file added app/assets/images/team/pedro-sousa.jpg
Binary file added app/assets/images/team/ricardo-lucena.jpg
Binary file added app/assets/images/team/rui-armada.jpg
Binary file added app/assets/images/team/rui-lopes.jpg
Binary file added app/assets/images/team/rui-oliveira.jpg
Binary file added app/assets/images/team/sofia-gomes.jpg
Binary file added app/assets/images/team/tiago-pereira.jpg
Binary file added app/assets/images/team/vitor-leite.jpg
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
130 changes: 130 additions & 0 deletions lib/atomic_web/live/profile_live/form.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
defmodule AtomicWeb.ProfileLive.FormComponent do
use AtomicWeb, :live_component

alias Atomic.Accounts
alias AtomicWeb.Components.ImageUploader
import AtomicWeb.Components.Forms
import AtomicWeb.Components.{Button, Avatar}

@impl true
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">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<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={:zinc} 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">
<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>
<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>
<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
def update(%{user: user} = assigns, socket) do
changeset = Accounts.change_user(user)

{:ok,
socket
|> allow_upload(:image,
accept: Uploaders.ProfilePicture.extension_whitelist(),
max_entries: 1
)
|> assign(assigns)
|> assign(:changeset, changeset)}
end

@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
socket.assigns.user
|> Accounts.change_user(user_params)
|> Map.put(:action, :validate)

{:noreply, assign(socket, :changeset, changeset)}
end

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

flash_text =
if user_params["email"] != user.email do
case Accounts.apply_user_email(user, %{email: user_params["email"]}) do
{:ok, applied_user} ->
Accounts.deliver_update_email_instructions(
applied_user,
user.email,
&url(~p"/users/confirm_email/#{&1}")
)

"Profile updated successfully, please check your email to confirm the new address."
end
else
"Profile updated successfully."
end

case Accounts.update_user(
user,
Map.put(user_params, "email", user.email),
&consume_image_data(socket, &1)
) do
{:ok, _user} ->
{:noreply,
socket
|> put_flash(:success, flash_text)
|> push_navigate(to: ~p"/profile/#{user_params["slug"] || user.slug}")}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end

defp consume_image_data(socket, user) do
consume_uploaded_entries(socket, :image, fn %{path: path}, entry ->
Accounts.update_user_picture(user, %{
"profile_picture" => %Plug.Upload{
content_type: entry.client_type,
filename: entry.client_name,
path: path
}
})
end)
|> case do
[{:ok, user}] ->
{:ok, user}

_errors ->
{:ok, user}
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<h2 class="hidden sm:block text-xl pb-4 font-bold leading-7 text-zinc-900 sm:text-4xl">
<%= gettext("Your Profile") %>
</h2>

<div class="flex flex-col justify-center">
<%= label(f, :name, class: "mb-1 text-sm font-medium text-zinc-700") %>
<%= text_input(f, :name,
Expand Down
88 changes: 0 additions & 88 deletions lib/atomic_web/live/profile_live/form_component.ex

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} 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-zinc-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_zinc} />
</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
Loading