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: improve image upload component #536

Draft
wants to merge 13 commits into
base: develop
Choose a base branch
from
17 changes: 14 additions & 3 deletions lib/atomic/uploader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,29 @@ defmodule Atomic.Uploader do
use Waffle.Definition
use Waffle.Ecto.Definition

def validate({file, _}) do
def validate(file, _) do
file_extension = file.file_name |> Path.extname() |> String.downcase()

case Enum.member?(extension_whitelist(), file_extension) do
true -> :ok
false -> {:error, "invalid file extension"}
true ->
if file.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_size, 500)
end
end
end
end
43 changes: 16 additions & 27 deletions lib/atomic_web/components/image_uploader.ex
Original file line number Diff line number Diff line change
@@ -1,43 +1,32 @@
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"""
<div>
<div id={@id}>
<div class="shrink-0 1.5xl:shrink-0">
<.live_file_input upload={@uploads.image} class="hidden" />
<div class={
"#{if length(@uploads.image.entries) != 0 do
"hidden"
end} border-2 border-gray-300 border-dashed rounded-md"
end} #{@class} border-2 border-gray-300 border-dashed rounded-md"
} phx-drop-target={@uploads.image.ref}>
<div class="mx-auto sm:col-span-6 lg:w-full">
<div class="my-[140px] flex justify-center px-6">
<div class="space-y-1 text-center">
<svg class="size-12 mx-auto text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="file-upload" class="relative cursor-pointer rounded-md font-medium text-orange-500 hover:text-red-800">
<a onclick={"document.getElementById('#{@uploads.image.ref}').click()"}>
Upload a file
</a>
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs text-gray-500">
PNG, JPG, GIF up to 10MB
</p>
<div class="flex h-full items-center justify-center px-6">
<div class="flex flex-col items-center justify-center space-y-1">
<svg class="size-12 mx-auto text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex flex-col items-center text-sm text-gray-600">
<label for="file-upload" class="relative cursor-pointer rounded-md font-medium text-orange-500 hover:text-red-800">
<a onclick={"document.getElementById('#{@uploads.image.ref}').click()"}>Upload a file</a>
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs text-gray-500">PNG, JPG, GIF up to <%= @size_file %></p>
</div>
</div>
</div>
Expand All @@ -52,9 +41,9 @@ defmodule AtomicWeb.Components.ImageUploader do
<div class="flex">
<figcaption>
<%= 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 %>
</figcaption>
<button type="button" phx-click="cancel-image" phx-target={@target} phx-value-ref={entry.ref} aria-label="cancel" class="pl-4">
Expand Down
2 changes: 1 addition & 1 deletion lib/atomic_web/components/sidebar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ defmodule AtomicWeb.Components.Sidebar do

defp user_image(user) do
if user.profile_picture do
Uploaders.ProfilePicture.url({user, user.profile_picture}, :original)
Uploaders.ProfilePicture.url({user.profile_picture, user}, :original)
else
nil
end
Expand Down
24 changes: 15 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,15 @@ defmodule AtomicWeb.ProfileLive.FormComponent do
use AtomicWeb, :live_component

alias Atomic.Accounts
alias AtomicWeb.Components.ImageUploader

@extensions_whitelist ~w(.jpg .jpeg .gif .png)

@impl true
def mount(socket) do
{:ok,
socket
|> allow_upload(:picture, accept: @extensions_whitelist, max_entries: 1)}
|> allow_upload(:image, accept: @extensions_whitelist, max_entries: 1)}
end

@impl true
Expand Down Expand Up @@ -68,21 +69,26 @@ defmodule AtomicWeb.ProfileLive.FormComponent do
end

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

_errors ->
{:ok, user}
end
consume_uploaded_entries(socket, :picture_2, fn %{path: path}, entry ->
Accounts.update_user(user, %{
"image_2" => %Plug.Upload{
content_type: entry.client_type,
filename: entry.client_name,
path: path
}
})
end)

{:ok, user}
end
end
30 changes: 3 additions & 27 deletions lib/atomic_web/live/profile_live/form_component.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -47,33 +47,9 @@
<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>
<%= 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_1" uploads={@uploads} target={@myself} class="h-100px w-100px" size_file="10KB" />
<.live_component module={ImageUploader} id="uploader-profile-picture_2" uploads={@uploads} target={@myself} class="h-100px w-100px" size_file="100KB" />
</div>
<div class="w-full flex flex-row-reverse mt-8">
<%= submit do %>
Expand Down
Loading