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

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
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
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
57 changes: 23 additions & 34 deletions lib/atomic_web/components/image_uploader.ex
Original file line number Diff line number Diff line change
@@ -1,60 +1,49 @@
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" />
<.live_file_input upload={@uploads} class="hidden" />
<div class={
"#{if length(@uploads.image.entries) != 0 do
"#{if length(@uploads.entries) != 0 do
"hidden"
end} border-2 border-zinc-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-zinc-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-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('#{@uploads.image.ref}').click()"}>
Upload a file
</a>
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs text-zinc-500">
PNG, JPG, GIF up to 10MB
</p>
end} #{@class} border-2 border-gray-300 border-dashed rounded-md"
} phx-drop-target={@uploads.ref}>
<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-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('#{@uploads.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>
<section>
<%= for entry <- @uploads.image.entries do %>
<%= for err <- upload_errors(@uploads.image, entry) do %>
<%= for entry <- @uploads.entries do %>
<%= for err <- upload_errors(@uploads, entry) do %>
<p class="alert alert-danger"><%= Phoenix.Naming.humanize(err) %></p>
<% end %>
<article class="upload-entry">
<figure class="w-[400px]">
<.live_img_preview entry={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 %>
<%= 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
37 changes: 28 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,24 @@ 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_1,
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
Expand All @@ -32,6 +42,10 @@ defmodule AtomicWeb.ProfileLive.FormComponent do
{:noreply, assign(socket, :changeset, changeset)}
end

def handle_event("cancel-image", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :image_1, ref)}
end

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

Expand Down Expand Up @@ -68,21 +82,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
27 changes: 3 additions & 24 deletions lib/atomic_web/live/profile_live/form_component.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +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-zinc-700") %>
<.live_component module={ImageUploader} id="uploader-profile-picture_1" uploads={@uploads.image_1} target={@myself} class="h-100px w-100px" size_file="10MB" />
<.live_component module={ImageUploader} id="uploader-profile-picture_2" uploads={@uploads.image_2} target={@myself} class="h-100px w-100px" size_file="100MB" />
</div>
<div class="w-full flex flex-row-reverse mt-8">
<%= submit do %>
Expand Down
Loading