diff --git a/lib/atomic/accounts.ex b/lib/atomic/accounts.ex index 676aa304d..ea8854fc7 100644 --- a/lib/atomic/accounts.ex +++ b/lib/atomic/accounts.ex @@ -491,7 +491,7 @@ defmodule Atomic.Accounts do {:error, %Ecto.Changeset{}} """ - def update_user(%User{} = user, attrs \\ %{}, _after_save \\ &{:ok, &1}) do + def update_user(%User{} = user, attrs \\ %{}) do user |> User.changeset(attrs) |> Repo.update() diff --git a/lib/atomic/uploader.ex b/lib/atomic/uploader.ex index 8bb59c9fe..eb6089f24 100644 --- a/lib/atomic/uploader.ex +++ b/lib/atomic/uploader.ex @@ -11,16 +11,32 @@ defmodule Atomic.Uploader do def validate({file, _}) do file_extension = file.file_name |> Path.extname() |> String.downcase() + size = file_size(file) case Enum.member?(extension_whitelist(), file_extension) do - true -> :ok - false -> {:error, "invalid file extension"} + true -> + if size <= max_size() do + :ok + else + {:error, "file size exceeds maximum allowed size"} + end + + false -> + {:error, "invalid file extension"} end end def extension_whitelist do Keyword.get(unquote(opts), :extensions, []) end + + def max_size do + Keyword.get(unquote(opts), :max_file_size, 100_000_000) + end + + def file_size(%Waffle.File{} = file) do + File.stat!(file.path) |> Map.get(:size) + end end end end diff --git a/lib/atomic_web/components/image_uploader.ex b/lib/atomic_web/components/image_uploader.ex index 39a00e1d8..bb0af43c5 100644 --- a/lib/atomic_web/components/image_uploader.ex +++ b/lib/atomic_web/components/image_uploader.ex @@ -1,60 +1,54 @@ defmodule AtomicWeb.Components.ImageUploader do @moduledoc """ An image uploader component that allows you to upload an image. - The component attributes are: - @uploads - the uploads object - @target - the target to send the event to - - The component events the parent component should define are: - cancel-image - cancels the upload of an image. This event should be defined in the component that you passed in the @target attribute. """ + use AtomicWeb, :live_component def render(assigns) do ~H""" -
+
- <.live_file_input upload={@uploads.image} class="hidden" /> + <.live_file_input upload={@upload} class="hidden" />
-
-
-
- -
- -

or drag and drop

-
-

- PNG, JPG, GIF up to 10MB -

+ end} #{@class} border-2 border-gray-300 border-dashed rounded-md" + } phx-drop-target={@upload.ref}> +
+
+ <.icon name={@icon} class="size-8 text-zinc-400" /> +
+ +

or drag and drop

+

+ <%= extensions_to_string(@upload.accept) %> up to <%= assigns.size_file %> <%= @type %> +

- <%= for entry <- @uploads.image.entries do %> - <%= for err <- upload_errors(@uploads.image, entry) do %> -

<%= Phoenix.Naming.humanize(err) %>

+ <%= for entry <- @upload.entries do %> + <%= for err <- upload_errors(@upload, entry) do %> + <% end %>
-
- <.live_img_preview entry={entry} /> +
+ <.live_img_preview entry={entry} id={"preview-#{entry.ref}"} class="rounded-lg shadow-lg" />
<%= if String.length(entry.client_name) < 30 do %> - <% entry.client_name %> + <%= entry.client_name %> <% else %> - <% String.slice(entry.client_name, 0..30) <> "... " %> + <%= String.slice(entry.client_name, 0..30) <> "... " %> <% end %>
""" end + + def update(assigns, socket) do + max_size = assigns.upload.max_file_size + type = assigns[:type] + + size_file = convert_size(max_size, type) + + {:ok, + socket + |> assign(assigns) + |> assign(:size_file, size_file)} + end + + defp convert_size(size_in_bytes, type) do + size_in_bytes_float = size_in_bytes * 1.0 + + case type 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) + "TB" -> Float.round(size_in_bytes_float / 1_000_000_000_000, 2) + _ -> size_in_bytes_float + end + end + + def extensions_to_string(extensions) do + extensions + |> String.split(",") + |> Enum.map_join(", ", fn ext -> String.trim_leading(ext, ".") |> String.upcase() end) + end end diff --git a/lib/atomic_web/live/profile_live/form_component.ex b/lib/atomic_web/live/profile_live/form_component.ex index b0e083345..600bc487f 100644 --- a/lib/atomic_web/live/profile_live/form_component.ex +++ b/lib/atomic_web/live/profile_live/form_component.ex @@ -2,6 +2,7 @@ defmodule AtomicWeb.ProfileLive.FormComponent do use AtomicWeb, :live_component alias Atomic.Accounts + alias AtomicWeb.Components.ImageUploader @extensions_whitelist ~w(.jpg .jpeg .gif .png) @@ -9,7 +10,16 @@ defmodule AtomicWeb.ProfileLive.FormComponent do def mount(socket) do {:ok, socket - |> allow_upload(:picture, accept: @extensions_whitelist, max_entries: 1)} + |> allow_upload(:profile_picture, + accept: @extensions_whitelist, + max_entries: 1, + max_file_size: 10_000_000 + ) + |> allow_upload(:image_2, + accept: @extensions_whitelist, + max_entries: 1, + max_file_size: 100_000_000 + )} end @impl true @@ -29,7 +39,24 @@ defmodule AtomicWeb.ProfileLive.FormComponent do |> Accounts.change_user(user_params) |> Map.put(:action, :validate) - {:noreply, assign(socket, :changeset, changeset)} + {:noreply, + socket + |> assign(:changeset, changeset)} + end + + def handle_event("cancel-image", %{"ref" => ref}, socket) do + uploads = [:profile_picture, :image_2] + + socket = + Enum.reduce(uploads, socket, fn key, acc -> + if Enum.any?(Map.get(acc.assigns.uploads, key, %{entries: []}).entries, &(&1.ref == ref)) do + cancel_upload(acc, key, ref) + else + acc + end + end) + + {:noreply, socket} end def handle_event("save", %{"user" => user_params}, socket) do @@ -51,38 +78,54 @@ defmodule AtomicWeb.ProfileLive.FormComponent do "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"]}")} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, :changeset, changeset)} + case Accounts.update_user(user, Map.put(user_params, "email", user.email)) do + {:ok, user} -> + case consume_image_data(socket, user) do + {:ok, _user} -> + {:noreply, + socket + |> put_flash(:success, flash_text) + |> push_navigate(to: ~p"/profile/#{user_params["slug"]}")} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end end end defp consume_image_data(socket, user) do - consume_uploaded_entries(socket, :image, fn %{path: path}, entry -> - Accounts.update_user(user, %{ - "image" => %Plug.Upload{ - content_type: entry.client_type, - filename: entry.client_name, - path: path - } - }) + consume_uploaded_entries(socket, :profile_picture, fn %{path: path}, entry -> + handle_image_upload(user, path, entry, :profile_picture) end) + + consume_uploaded_entries(socket, :image_2, fn %{path: path}, entry -> + handle_image_upload(user, path, entry, :image_2) + end) + + {:ok, user} + end + + defp handle_image_upload(user, path, entry, field) do + Accounts.update_user_picture(user, %{ + "#{field}" => %Plug.Upload{ + content_type: entry.client_type, + filename: entry.client_name, + path: path + } + }) |> case do - [{:ok, user}] -> + {:ok, user} -> {:ok, user} - _errors -> - {:ok, user} + {:error, changeset} -> + if changeset.errors[field] do + {:postpone, "File size exceeds maximum allowed size"} + else + {:error, changeset} + end + + {:errors, _changeset} -> + {:error, "An error occurred while updating the user."} end end end diff --git a/lib/atomic_web/live/profile_live/form_component.html.heex b/lib/atomic_web/live/profile_live/form_component.html.heex index f991659f8..463afeabe 100644 --- a/lib/atomic_web/live/profile_live/form_component.html.heex +++ b/lib/atomic_web/live/profile_live/form_component.html.heex @@ -47,30 +47,9 @@
<%= error_tag(f, :phone_number) %>
- <.live_file_input upload={@uploads.picture} class="hidden" /> - -
-
- <.icon name="hero-camera" class="mx-auto w-12 h-12 sm:w-20 sm:h-20 text-white group-hover:text-opacity-70" /> -
-
-
- <%= for entry <- @uploads.picture.entries do %> - <%= for err <- upload_errors(@uploads.picture, entry) do %> -

<%= Phoenix.Naming.humanize(err) %>

- <% end %> -
-
- <.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" /> -
-
- <.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" /> -
-
- <% end %> -
-
+ <%= label(f, :name, "Profile Picture", class: "mt-3 mb-1 text-sm font-medium text-zinc-700") %> + <.live_component module={ImageUploader} icon="hero-camera" id="uploader-profile-picture_1" upload={@uploads.profile_picture} target={@myself} class="h-100px w-100px" type="MB" /> + <.live_component module={ImageUploader} icon="hero-photo" id="uploader-profile-picture_2" upload={@uploads.image_2} target={@myself} class="h-100px w-100px" type="GB" />
<%= submit do %> diff --git a/lib/atomic_web/live/profile_live/show.html.heex b/lib/atomic_web/live/profile_live/show.html.heex index 40ce92cb2..797824230 100644 --- a/lib/atomic_web/live/profile_live/show.html.heex +++ b/lib/atomic_web/live/profile_live/show.html.heex @@ -19,7 +19,6 @@
<% end %> - <%= if @user.phone_number do %>
Phone
@@ -32,7 +31,11 @@ <% end %>
- <.avatar class="sm:w-44 sm:h-44 sm:text-6xl" name={@user.name} size={:xl} color={:light_zinc} /> + <%= if !@user.profile_picture do %> + <.avatar class="sm:w-44 sm:h-44 sm:text-6xl" name={@user.name} size={:xl} color={:light_zinc} /> + <% else %> + <.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)} /> + <% end %>