Last active
June 8, 2023 15:31
-
-
Save Neophen/f90176a17fad823d1957c7245d70beec to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule PentoWeb.UploadLive do | |
use PentoWeb, :live_view | |
# @bytes_for_5MB 5_242_880 | |
@bytes_for_5MB 1_000_000 | |
@impl Phoenix.LiveView | |
def mount(_params, _session, socket) do | |
socket = | |
socket | |
|> assign(:uploaded_files, []) | |
|> assign(:hidden_inputs, []) | |
|> allow_upload(:avatar, | |
accept: ~w(image/*), | |
max_entries: 1, | |
max_file_size: @bytes_for_5MB, | |
auto_upload: true, | |
external: &presign_upload/2 | |
) | |
{:ok, socket} | |
end | |
@impl Phoenix.LiveView | |
def render(assigns) do | |
~H""" | |
<form id="upload-form" phx-submit="save" phx-change="validate"> | |
<input :for={{id, value} <- @hidden_inputs} type="hidden" id={id} name={id} value={value} /> | |
<div class="relative max-w-xl"> | |
<.live_file_input upload={@uploads.avatar} class="hidden" /> | |
<div class="min-h-max w-full"> | |
<label | |
:if={@uploads.avatar.entries == [] && @uploaded_files == []} | |
for={@uploads.avatar.ref} | |
phx-drop-target={@uploads.avatar.ref} | |
class="relative flex aspect-square w-full cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-600 bg-gray-100 px-6 transition-colors hover:bg-gray-200" | |
> | |
<div class="flex gap-1 text-gray-600"> | |
<.icon name="hero-camera" class="h-6 w-6" /> | |
<div class="grid gap-1"> | |
<p class="text-body-1">Click to add a photo</p> | |
<p class="text-caption-2 text-gray-500">JPEG, PNG or TIFF (max. 5 MB)</p> | |
</div> | |
</div> | |
</label> | |
<.image_preview :for={entry <- @uploads.avatar.entries} entry={entry} live? /> | |
<.image_preview :for={entry <- @uploaded_files} entry={entry} /> | |
</div> | |
<div class="text-caption-2 mt-2 text-red-400 "> | |
<p :for={err <- upload_errors(@uploads.avatar)}><%= error_to_string(err) %></p> | |
<%= for entry <- @uploads.avatar.entries do %> | |
<p :for={err <- upload_errors(@uploads.avatar, entry)}><%= error_to_string(err) %></p> | |
<% end %> | |
</div> | |
</div> | |
<button type="submit" class="block p-4 border hover:bg-slate-200">Upload</button> | |
</form> | |
<.image_preview_modal :for={entry <- @uploads.avatar.entries} entry={entry} live? /> | |
<.image_preview_modal :for={entry <- @uploaded_files} entry={entry} /> | |
""" | |
end | |
@impl Phoenix.LiveView | |
def handle_event("validate", params, socket) do | |
hidden_inputs = | |
socket.assigns.hidden_inputs | |
|> Enum.map(fn {id, value} -> {id, Map.get(params, id) || value} end) | |
socket = assign(socket, :hidden_inputs, hidden_inputs) | |
{:noreply, socket} | |
end | |
def handle_event("remove-entry", %{"value" => ref, "live?" => true}, socket) do | |
socket = cancel_upload(socket, :avatar, ref) | |
{:noreply, socket} | |
end | |
def handle_event("remove-entry", %{"value" => ref}, socket) do | |
# remove from the service? | |
socket = assign(socket, :uploaded_files, []) | |
{:noreply, socket} | |
end | |
def handle_event("save", params, socket) do | |
uploaded_files = | |
consume_uploaded_entries(socket, :avatar, fn meta, entry -> | |
meta = params |> Map.get(meta.input_id) |> Jason.decode!() | |
uuid = Map.get(meta, "uuid") | |
entry = %{ | |
url: Map.get(meta, "cdnUrl"), | |
ref: uuid, | |
uuid: uuid, | |
progress: 100 | |
} | |
{:ok, entry} | |
end) | |
socket = assign(socket, :uploaded_files, uploaded_files) | |
{:noreply, socket} | |
end | |
# Helpers __________________________________________________________________________________________________________ | |
defp presign_upload(entry, socket) do | |
input_id = "input-" <> entry.uuid | |
meta = %{uploader: "Uploadcare", input_id: input_id} | |
hidden_inputs = (socket.assigns.hidden_inputs ++ [{input_id, ""}]) |> Enum.uniq() | |
socket = assign(socket, :hidden_inputs, hidden_inputs) | |
{:ok, meta, socket} | |
end | |
def error_to_string(:too_large), do: "Too large" | |
def error_to_string(:not_accepted), do: "You have selected an unacceptable file type" | |
def error_to_string(:too_many_files), do: "You have selected too many files" | |
# Components | |
attr :entry, :map, required: true | |
attr :live?, :boolean, default: false | |
defp image_preview(assigns) do | |
~H""" | |
<div class="w-full aspect-square relative"> | |
<.live_img_preview :if={@live?} entry={@entry} class="h-full w-full rounded-md object-cover" /> | |
<img | |
:if={not @live?} | |
src={@entry.url <> "-/smart_resize/300x300/"} | |
key={"preview-#{@entry.uuid}"} | |
class="h-full w-full rounded-md object-cover" | |
/> | |
<button | |
type="button" | |
onclick={"document.getElementById('#{@entry.uuid}').showModal()"} | |
class="absolute inset-0 w-full h-full bg-gradient-to-b from-black/50 hover:from-indigo-400 rounded-md transition-colors" | |
> | |
</button> | |
<div :if={@entry.progress < 100} class="absolute inset-0 w-full h-full bg-black/50"> | |
<div | |
class="h-full bg-teal-400/50 transition-transform scale-x-0 will-change-transform origin-left" | |
style={"--tw-scale-x: #{@entry.progress * 0.01}"} | |
> | |
</div> | |
</div> | |
<button | |
type="button" | |
phx-click={JS.push("remove-entry", value: %{value: @entry.ref, live?: @live?})} | |
aria-label="delete" | |
class="absolute right-0 top-0 m-2" | |
> | |
<div class="text-geraldine-400 flex h-12 w-12 rounded bg-white items-center justify-center hover:bg-gray-100"> | |
<.icon name="hero-trash" class="h-6 w-6" /> | |
</div> | |
</button> | |
</div> | |
""" | |
end | |
attr :entry, :map, required: true | |
attr :live?, :boolean, default: false | |
defp image_preview_modal(assigns) do | |
~H""" | |
<dialog | |
id={@entry.uuid} | |
class="hidden open:block backdrop:bg-[#150A2DCC] bg-transparent max-w-2xl w-full p-4 sm:p-6" | |
> | |
<.live_img_preview | |
:if={@live?} | |
id={"modal-#{@entry.uuid}"} | |
entry={@entry} | |
class="w-full rounded-md" | |
/> | |
<img :if={not @live?} src={@entry.url <> "-/smart_resize/1200x1200/"} class="w-full rounded-md" /> | |
<button | |
type="button" | |
onclick={"document.getElementById('#{@entry.uuid}').close()"} | |
class="fixed inset-0 w-full h-full" | |
> | |
</button> | |
</dialog> | |
""" | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { UploadClient } from '@uploadcare/upload-client' | |
const client = new UploadClient({ publicKey: "somekey" }) | |
const markCompleted = (uploadedEntry) => { | |
uploadedEntry.progress(100) | |
} | |
const Uploadcare = (entries, onViewError) => { | |
const uploadEntry = makeEntryUploader(onViewError) | |
entries.forEach(uploadEntry) | |
} | |
const makeEntryUploader = (onViewError) => async (entry) => { | |
const abortController = new AbortController() | |
onViewError(() => abortController.abort()) | |
const { file, meta } = entry | |
const onProgress = ({ isComputable, value }) => { | |
if (isComputable) { | |
let percent = Math.round(value * 100) | |
// It's important to not set progress to 100 as that completes the upload | |
if (percent < 100) { entry.progress(percent) } | |
} | |
} | |
const result = await client.uploadFile(file, { | |
onProgress, | |
signal: abortController.signal, | |
}) | |
markCompleted(entry) | |
// Fire off event to update the hidden input so | |
// that we can grab the meta data server side | |
meta_input = document.getElementById(meta.input_id) | |
const meta_data = JSON.stringify(result) | |
meta_input.value = meta_data | |
meta_input.dispatchEvent(new Event('input', { bubbles: true })); | |
} | |
const uploaders = { | |
Uploadcare, | |
} | |
export default uploaders |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment