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 26 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
64f8726
Update 22/23 Team (#249)
ruioliveira02 Oct 21, 2022
ef699a9
Update Felicio picture (#250)
feliciofilipe Oct 21, 2022
e1f06cd
Add partners (#282)
MarioRodrigues10 Apr 22, 2023
99b28b4
feat: initial profile page
AfonsoMartins26 Sep 23, 2024
ea9e7cc
fix: save redirect
AfonsoMartins26 Sep 23, 2024
7063235
Fix: save redirect Tiktok
AfonsoMartins26 Sep 24, 2024
90093c9
Chore: improve layout of profile page
AfonsoMartins26 Sep 24, 2024
0bab33d
Feat: Improve html
AfonsoMartins26 Sep 24, 2024
7cc2a00
misspell
AfonsoMartins26 Sep 24, 2024
95cbd6a
Fix: Profile Picture upload
AfonsoMartins26 Sep 25, 2024
ad5460f
format code
AfonsoMartins26 Sep 26, 2024
21741f0
Merge branch 'develop' into am/profile
AfonsoMartins26 Sep 28, 2024
07b3111
feat: preview profile image in edit page
AfonsoMartins26 Oct 1, 2024
d44469c
Merge branch 'am/profile' of github.com:cesium/atomic into am/profile
AfonsoMartins26 Oct 1, 2024
da8d768
fix: miss div
AfonsoMartins26 Oct 1, 2024
1854bf2
feat: add slug field
AfonsoMartins26 Oct 3, 2024
e9c9aec
fix: delete IO
AfonsoMartins26 Oct 3, 2024
85b8d58
feat: pass checks
AfonsoMartins26 Oct 3, 2024
95b9214
merge with main
AfonsoMartins26 Jan 9, 2025
4a053dd
merge develop
AfonsoMartins26 Jan 28, 2025
2ea657f
Merge branch 'develop' into am/profile
AfonsoMartins26 Mar 4, 2025
e806f1c
feat: merge main
AfonsoMartins26 Mar 4, 2025
1efa016
feat: merge develop
AfonsoMartins26 Mar 4, 2025
e78f8f4
feat: update image upload componente and banner working
AfonsoMartins26 Mar 6, 2025
0049f88
feat: change Uploaders
AfonsoMartins26 Mar 7, 2025
49d994a
feat: try to fix problem
AfonsoMartins26 Mar 9, 2025
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 \\ %{}) 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
8 changes: 6 additions & 2 deletions 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 All @@ -32,13 +33,16 @@ defmodule Atomic.Accounts.User do
field :confirmed_at, :naive_datetime
field :phone_number, :string
field :profile_picture, Uploaders.ProfilePicture.Type
field :banner, Uploaders.ProfilePicture.Type

belongs_to :course, Course
belongs_to :current_organization, Organization

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,8 +74,7 @@ defmodule Atomic.Accounts.User do

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

@doc """
Expand All @@ -83,6 +86,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
11 changes: 5 additions & 6 deletions lib/atomic/uploaders/banner.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
defmodule Atomic.Uploaders.Banner do
@moduledoc """
Uploader for department banners.
Uploader for user banners.
"""
use Atomic.Uploader, extensions: ~w(.jpg .jpeg .png)

alias Atomic.Organizations.Department
use Atomic.Uploader, extensions: ~w(.jpg .jpeg .png .gif)
alias Atomic.Accounts.User

@versions [:original]

def storage_dir(_version, {_file, %Department{} = department}) do
"uploads/atomic/departments/#{department.id}/banner"
def storage_dir(_version, {_file, %User{} = user}) do
"uploads/atomic/users/#{user.id}/banner"
end

def filename(version, _) do
Expand Down
135 changes: 76 additions & 59 deletions lib/atomic_web/components/image_uploader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,100 @@ defmodule AtomicWeb.Components.ImageUploader do
An image uploader component that allows you to upload an image.
"""

use AtomicWeb, :live_component
use AtomicWeb, :component

attr :id, :string, default: "image-uploader"
attr :upload, :any, required: true
attr :class, :string, default: ""
attr :image_class, :string, default: ""
attr :image, :string, default: nil
attr :icon, :string, default: "hero-photo"
attr :preview_disabled, :boolean, default: false
attr :rounded, :boolean, default: false
attr :memory_unit, :string, default: "MB"

slot :placeholder, optional: true, doc: "Slot for the placeholder content."

def image_uploader(assigns) do
assigns = update(assigns, %{})

def render(assigns) do
~H"""
<div id={@id}>
<div class="shrink-0 1.5xl:shrink-0">
<.live_file_input upload={@upload} class="hidden" />
<div class={
"#{if length(@upload.entries) != 0 do
"hidden"
end} #{@class} border-2 border-gray-300 border-dashed rounded-md"
} phx-drop-target={@upload.ref}>
<div class="flex h-full items-center justify-center px-6">
<div class="flex flex-col items-center justify-center space-y-1">
<.icon name={@icon} class="size-8 text-zinc-400" />
<div class="flex flex-col items-center text-sm text-zinc-600">
<label for="file-upload" class="relative cursor-pointer rounded-md font-medium text-orange-500 hover:text-red-800">
<a onclick={"document.getElementById('#{@upload.ref}').click()"}>Upload a file</a>
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs text-gray-500">
<%= extensions_to_string(@upload.accept) %> up to <%= assigns.size_file %> <%= @type %>
</p>
</div>
</div>
</div>
<section>
<.live_file_input upload={@upload} class="hidden" />
<section
phx-drop-target={@upload.ref}
class={[
"transition-colors hover:cursor-pointer hover:bg-lightShade/30 dark:hover:bg-darkShade/20 border-2 border-dashed border-lightShade dark:border-darkShade",
@rounded && "rounded-full overflow-hidden",
not @rounded && "rounded-xl",
@class
]}
onclick={"document.getElementById('#{@upload.ref}').click()"}
>
<%= if @upload.entries == [] do %>
<article class="h-full">
<figure class="flex h-full items-center justify-center">
<%= if @image do %>
<img class={[@rounded && "p-0", not @rounded && "p-4", @image_class]} src={@image} />
<% else %>
<%= if @placeholder do %>
<div class="text-lightMuted flex flex-col items-center gap-2 dark:text-darkMuted">
<%= render_slot(@placeholder) %>
<p class="text-xs text-gray-500">
<%= extensions_to_string(@upload.accept) %> up to <%= @size_file %> <%= @memory_unit %>
</p>
</div>
<% else %>
<div class="text-lightMuted flex select-none flex-col items-center gap-2 dark:text-darkMuted">
<.icon name={@icon} class="h-12 w-12" />
<p class="px-4 text-center"><%= gettext("Upload a file or drag and drop.") %></p>
</div>
<% end %>
<% end %>
</figure>
</article>
<% end %>
<%= if !@preview_disabled do %>
<%= for entry <- @upload.entries do %>
<%= for err <- upload_errors(@upload, entry) do %>
<div class="alert alert-danger relative rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700" role="alert">
<span class="block sm:inline"><%= Phoenix.Naming.humanize(err) %></span>
<span class="absolute top-0 right-0 bottom-0 px-4 py-3">
<title>Close</title>
</span>
</div>
<% end %>
<article class="upload-entry">
<figure class="w-[100px]">
<.live_img_preview entry={entry} id={"preview-#{entry.ref}"} class="rounded-lg shadow-lg" />
<div class="flex">
<figcaption>
<%= if String.length(entry.client_name) < 30 do %>
<%= entry.client_name %>
<% else %>
<%= String.slice(entry.client_name, 0..30) <> "... " %>
<% end %>
</figcaption>
<button type="button" phx-click="cancel-image" phx-target={@target} phx-value-ref={entry.ref} aria-label="cancel" class="pl-4">
<.icon name="hero-x-mark-solid" class="size-5 text-zinc-400" />
</button>
</div>
<article class="h-full">
<figure class="flex h-full items-center justify-center">
<%= if entry.ref do %>
<.live_img_preview id={"preview-#{entry.ref}"} class={[@rounded && "p-0", not @rounded && "p-4", @image_class]} entry={entry} />
<% else %>
<div class="text-lightMuted flex select-none flex-col items-center gap-2 dark:text-darkMuted">
<.icon name="hero-document" class="h-12 w-12" />
<p class="px-4 text-center"><%= entry.client_name %></p>
</div>
<% end %>
</figure>
<%= for err <- upload_errors(@upload, entry) do %>
<p class="alert alert-danger"><%= Phoenix.Naming.humanize(err) %></p>
<% end %>
</article>
<% end %>
</section>
</div>
<% end %>
<%= for err <- upload_errors(@upload) do %>
<p class="alert alert-danger"><%= Phoenix.Naming.humanize(err) %></p>
<% end %>
</section>
</div>
"""
end

def update(assigns, socket) do
def update(assigns, _socket) do
max_size = assigns.upload.max_file_size
type = assigns[:type]
memory_unit = assigns[:memory_unit]

size_file = convert_size(max_size, type)
size_file = convert_size(max_size, memory_unit)

{:ok,
socket
|> assign(assigns)
|> assign(:size_file, size_file)}
assigns
|> Map.put(:size_file, size_file)
end

defp convert_size(size_in_bytes, type) do
defp convert_size(size_in_bytes, memory_unit) do
size_in_bytes_float = size_in_bytes * 1.0

case type do
case memory_unit do
"kB" -> Float.round(size_in_bytes_float / 1_000, 2)
"MB" -> Float.round(size_in_bytes_float / 1_000_000, 2)
"GB" -> Float.round(size_in_bytes_float / 1_000_000_000, 2)
Expand Down
2 changes: 1 addition & 1 deletion lib/atomic_web/live/profile_live/edit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule AtomicWeb.ProfileLive.Edit do
def handle_params(%{"slug" => user_slug}, _, socket) do
user = Accounts.get_user_by_slug(user_slug)

if socket.assigns.current_user.slug == user_slug do
if socket.assigns.current_user && socket.assigns.current_user.slug == user_slug do
{:noreply,
socket
|> assign(:page_title, user.name)
Expand Down
82 changes: 82 additions & 0 deletions lib/atomic_web/live/profile_live/form.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<div class="pt-4 px-4">
<.form :let={f} for={@changeset} id="profile-form" phx-target={@myself} phx-change="validate" phx-submit="save">
<div class="flex flex-col-reverse sm:flex-row items-center sm:justify-between mt-4 border-b pb-10 border-zinc-200">
<div class="space-y-4 w-full">
<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,
required: true,
placeholder: gettext("John Doe"),
class: "relative w-full sm:w-96 appearance-none rounded border border-zinc-300 px-3 py-2 text-zinc-900 placeholder-zinc-500 focus:z-10 focus:border-primary-400 focus:ring-primary-400 focus:outline-none sm:text-sm"
) %>
<div class="flex flex-col text-sm w-full sm:w-96 text-red-600"><%= error_tag(f, :name) %></div>
</div>
<div class="flex flex-col justify-center">
<%= label(f, :email, class: "mb-1 text-sm font-medium text-zinc-700") %>
<%= text_input(f, :email,
required: true,
placeholder: gettext("john_doe@mail.com"),
class: "relative w-full sm:w-96 appearance-none rounded border border-zinc-300 px-3 py-2 text-zinc-900 placeholder-zinc-500 focus:z-10 focus:border-primary-400 focus:ring-primary-400 focus:outline-none sm:text-sm"
) %>
<div class="flex flex-col text-sm w-full sm:w-96 text-red-600"><%= error_tag(f, :email) %></div>
</div>
<div class="flex flex-col justify-center">
<%= label(f, :username, class: "mb-1 text-sm font-medium text-zinc-700") %>
<div class="flex flex-row relative w-full sm:w-96 appearance-none rounded border border-zinc-300 px-3 text-zinc-900 placeholder-zinc-500 focus-within:z-10 focus-within:border-primary-400 focus-within:ring-primary-400 focus-within:outline-none sm:text-sm">
<span class="self-center select-none">@</span>
<%= text_input(f, :slug,
required: true,
spellcheck: false,
placeholder: gettext("john_doe"),
class: "pl-0 appearance-none text-zinc-900 placeholder-zinc-500 sm:text-sm border-none w-full focus:outline-none focus:ring-transparent"
) %>
</div>
<div class="flex flex-col text-sm w-full sm:w-96 text-red-600"><%= error_tag(f, :slug) %></div>
</div>
<div class="flex flex-col justify-center">
<%= label(f, :phone_number, class: "mb-1 text-sm font-medium text-zinc-700") %>
<%= text_input(f, :phone_number,
required: true,
placeholder: gettext("912345678"),
class: "relative w-full sm:w-96 appearance-none rounded border border-zinc-300 px-3 py-2 text-zinc-900 placeholder-zinc-500 focus:z-10 focus:border-primary-400 focus:ring-primary-400 focus:outline-none sm:text-sm"
) %>
<div class="flex flex-col text-sm w-full sm:w-96 text-red-600"><%= error_tag(f, :phone_number) %></div>
</div>
</div>
<.live_file_input upload={@uploads.picture} class="hidden" />
<a onclick={"document.getElementById('#{@uploads.picture.ref}').click()"}>
<div class={
"#{if length(@uploads.picture.entries) != 0 do "hidden" end} relative w-40 h-40 ring-2 ring-zinc-300 rounded-full cursor-pointer bg-zinc-400 sm:w-48 group sm:h-48 hover:bg-tertiary"}>
<div class="flex absolute justify-center items-center w-full h-full">
<.icon name="hero-camera" class="mx-auto w-12 h-12 sm:w-20 sm:h-20 text-white group-hover:text-opacity-70" />
</div>
</div>
<section>
<%= for entry <- @uploads.picture.entries do %>
<%= for err <- upload_errors(@uploads.picture, entry) do %>
<p class="alert alert-danger"><%= Phoenix.Naming.humanize(err) %></p>
<% end %>
<article class="flex relative items-center w-40 h-40 sm:w-48 sm:h-48 bg-white rounded-full cursor-pointer upload-entry group">
<div class="flex absolute z-10 justify-center items-center w-full h-full rounded-full">
<.icon name="hero-camera" class="mx-auto w-12 h-12 sm:w-20 sm:h-20 text-white text-opacity-0 rounded-full group-hover:text-opacity-100" />
</div>
<figure class="flex justify-center items-center w-full h-full rounded-full group-hover:opacity-80">
<.live_img_preview entry={entry} class="object-cover object-center rounded-full w-40 h-40 sm:w-48 sm:h-48 border-4 border-white" />
</figure>
</article>
<% end %>
</section>
</a>
</div>
<div class="w-full flex flex-row-reverse mt-8">
<%= submit do %>
<div class="inline-flex px-6 py-2 text-sm font-medium text-primary-500 bg-white border-2 border-primary-500 rounded-md shadow-sm hover:bg-primary-600 hover:text-white">
<.icon name="hero-archive-box-solid" class="mr-2 -ml-1 w-5 h-5" /> Save
</div>
<% end %>
</div>
</.form>
</div>
Loading