feat(elixir): image upload function with preview
This commit is contained in:
@@ -17,7 +17,7 @@ defmodule ComponentsElixirWeb do
|
||||
those modules here.
|
||||
"""
|
||||
|
||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
||||
def static_paths, do: ~w(assets fonts images uploads favicon.ico robots.txt)
|
||||
|
||||
def router do
|
||||
quote do
|
||||
|
||||
@@ -31,6 +31,13 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|> assign(:show_edit_form, false)
|
||||
|> assign(:editing_component, nil)
|
||||
|> assign(:form, nil)
|
||||
|> assign(:show_image_modal, false)
|
||||
|> assign(:modal_image_url, nil)
|
||||
|> allow_upload(:image,
|
||||
accept: ~w(.jpg .jpeg .png .gif),
|
||||
max_entries: 1,
|
||||
max_file_size: 5_000_000
|
||||
)
|
||||
|> load_components()}
|
||||
end
|
||||
end
|
||||
@@ -128,6 +135,9 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|
||||
case Inventory.delete_component(component) do
|
||||
{:ok, _deleted_component} ->
|
||||
# Clean up the image file if it exists
|
||||
delete_image_file(component.image_filename)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Component deleted")
|
||||
@@ -175,8 +185,37 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|> assign(:form, nil)}
|
||||
end
|
||||
|
||||
def handle_event("show_image", %{"url" => url}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:show_image_modal, true)
|
||||
|> assign(:modal_image_url, url)}
|
||||
end
|
||||
|
||||
def handle_event("close_image_modal", _params, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:show_image_modal, false)
|
||||
|> assign(:modal_image_url, nil)}
|
||||
end
|
||||
|
||||
def handle_event("prevent_close", _params, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("validate", _params, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("cancel-upload", %{"ref" => ref}, socket) do
|
||||
{:noreply, cancel_upload(socket, :image, ref)}
|
||||
end
|
||||
|
||||
def handle_event("save_component", %{"component" => component_params}, socket) do
|
||||
case Inventory.create_component(component_params) do
|
||||
# Handle any uploaded images
|
||||
updated_params = save_uploaded_image(socket, component_params)
|
||||
|
||||
case Inventory.create_component(updated_params) do
|
||||
{:ok, _component} ->
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -191,7 +230,10 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
end
|
||||
|
||||
def handle_event("save_edit", %{"component" => component_params}, socket) do
|
||||
case Inventory.update_component(socket.assigns.editing_component, component_params) do
|
||||
# Handle any uploaded images
|
||||
updated_params = save_uploaded_image(socket, component_params)
|
||||
|
||||
case Inventory.update_component(socket.assigns.editing_component, updated_params) do
|
||||
{:ok, _component} ->
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -364,7 +406,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<.form for={@form} phx-submit="save_component" class="space-y-4">
|
||||
<.form for={@form} phx-submit="save_component" phx-change="validate" multipart={true} class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<.input field={@form[:name]} type="text" required />
|
||||
@@ -407,6 +449,40 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Component Image</label>
|
||||
<div class="mt-1">
|
||||
<.live_file_input upload={@uploads.image} class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
JPG, PNG, GIF up to 5MB
|
||||
</p>
|
||||
|
||||
<%= for err <- upload_errors(@uploads.image) do %>
|
||||
<p class="text-red-600 text-sm mt-1"><%= Phoenix.Naming.humanize(err) %></p>
|
||||
<% end %>
|
||||
|
||||
<%= for entry <- @uploads.image.entries do %>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<.live_img_preview entry={entry} class="h-10 w-10 rounded-lg object-cover" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900"><%= entry.client_name %></p>
|
||||
<p class="text-sm text-gray-500"><%= entry.progress %>%</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref} aria-label="cancel" class="text-red-600 hover:text-red-900">
|
||||
<.icon name="hero-x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= for err <- upload_errors(@uploads.image) do %>
|
||||
<p class="mt-1 text-sm text-red-600"><%= upload_error_to_string(err) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -443,7 +519,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<.form for={@form} phx-submit="save_edit" class="space-y-4">
|
||||
<.form for={@form} phx-submit="save_edit" phx-change="validate" multipart={true} class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<.input field={@form[:name]} type="text" required />
|
||||
@@ -486,6 +562,46 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Component Image</label>
|
||||
<%= if @editing_component && @editing_component.image_filename do %>
|
||||
<div class="mt-1 mb-2">
|
||||
<p class="text-sm text-gray-600">Current image:</p>
|
||||
<img src={"/uploads/images/#{@editing_component.image_filename}"} alt="Current component" class="h-20 w-20 object-cover rounded-lg" />
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="mt-1">
|
||||
<.live_file_input upload={@uploads.image} class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
JPG, PNG, GIF up to 5MB (leave empty to keep current image)
|
||||
</p>
|
||||
|
||||
<%= for err <- upload_errors(@uploads.image) do %>
|
||||
<p class="text-red-600 text-sm mt-1"><%= Phoenix.Naming.humanize(err) %></p>
|
||||
<% end %>
|
||||
|
||||
<%= for entry <- @uploads.image.entries do %>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<.live_img_preview entry={entry} class="h-10 w-10 rounded-lg object-cover" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900"><%= entry.client_name %></p>
|
||||
<p class="text-sm text-gray-500"><%= entry.progress %>%</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref} aria-label="cancel" class="text-red-600 hover:text-red-900">
|
||||
<.icon name="hero-x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= for err <- upload_errors(@uploads.image) do %>
|
||||
<p class="mt-1 text-sm text-red-600"><%= upload_error_to_string(err) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -514,22 +630,39 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
<%= for component <- @components do %>
|
||||
<li id={"component-#{component.id}"} class="px-6 py-4 hover:bg-gray-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Component Image -->
|
||||
<div class="flex-shrink-0 mr-4">
|
||||
<%= if component.image_filename do %>
|
||||
<button phx-click="show_image" phx-value-url={"/uploads/images/#{component.image_filename}"} class="hover:opacity-75 transition-opacity">
|
||||
<img src={"/uploads/images/#{component.image_filename}"} alt={component.name} class="h-12 w-12 rounded-lg object-cover cursor-pointer" />
|
||||
</button>
|
||||
<% else %>
|
||||
<div class="h-12 w-12 rounded-lg bg-gray-200 flex items-center justify-center">
|
||||
<.icon name="hero-cube-transparent" class="h-6 w-6 text-gray-400" />
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-indigo-600 truncate">
|
||||
<%= if component.datasheet_url do %>
|
||||
<a
|
||||
href={component.datasheet_url}
|
||||
target="_blank"
|
||||
class="hover:text-indigo-500"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm font-medium text-indigo-600 truncate">
|
||||
<%= if component.datasheet_url do %>
|
||||
<a
|
||||
href={component.datasheet_url}
|
||||
target="_blank"
|
||||
class="hover:text-indigo-500"
|
||||
>
|
||||
{component.name}
|
||||
</a>
|
||||
<% else %>
|
||||
{component.name}
|
||||
<.icon name="hero-arrow-top-right-on-square" class="w-4 h-4 inline ml-1" />
|
||||
</a>
|
||||
<% else %>
|
||||
{component.name}
|
||||
<% end %>
|
||||
</p>
|
||||
<%= if component.datasheet_url do %>
|
||||
<span class="ml-2 text-blue-500" title="Datasheet available">📄</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-2 flex-shrink-0 flex">
|
||||
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
{component.category.name}
|
||||
@@ -631,6 +764,83 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Modal -->
|
||||
<%= if @show_image_modal do %>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4" phx-click="close_image_modal">
|
||||
<!-- Background overlay -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-75"></div>
|
||||
|
||||
<!-- Modal content -->
|
||||
<div class="relative bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-full overflow-auto" phx-click="prevent_close">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center p-4 border-b bg-white rounded-t-lg">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Component Image</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 hover:text-gray-600 text-3xl font-bold leading-none p-1"
|
||||
phx-click="close_image_modal"
|
||||
title="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6 bg-white rounded-b-lg">
|
||||
<div class="text-center">
|
||||
<%= if @modal_image_url do %>
|
||||
<img
|
||||
src={@modal_image_url}
|
||||
alt="Component image"
|
||||
class="max-w-full max-h-[70vh] mx-auto rounded-lg shadow-sm"
|
||||
style="object-fit: contain;"
|
||||
/>
|
||||
<% else %>
|
||||
<p class="text-gray-500 py-8">No image available</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
# Helper functions for image upload handling
|
||||
defp save_uploaded_image(socket, component_params) do
|
||||
uploaded_files =
|
||||
consume_uploaded_entries(socket, :image, fn %{path: path}, entry ->
|
||||
filename = "#{System.unique_integer([:positive])}_#{entry.client_name}"
|
||||
dest = Path.join(["priv", "static", "uploads", "images", filename])
|
||||
|
||||
# Ensure the upload directory exists
|
||||
File.mkdir_p!(Path.dirname(dest))
|
||||
|
||||
# Copy the file
|
||||
case File.cp(path, dest) do
|
||||
:ok -> filename
|
||||
{:error, _reason} -> nil
|
||||
end
|
||||
end)
|
||||
|
||||
case uploaded_files do
|
||||
[filename] when is_binary(filename) -> Map.put(component_params, "image_filename", filename)
|
||||
[] -> component_params
|
||||
_error -> component_params
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_image_file(nil), do: :ok
|
||||
defp delete_image_file(""), do: :ok
|
||||
|
||||
defp delete_image_file(filename) do
|
||||
path = Path.join(["priv", "static", "uploads", "images", filename])
|
||||
File.rm(path)
|
||||
end
|
||||
|
||||
defp upload_error_to_string(:too_large), do: "File too large"
|
||||
defp upload_error_to_string(:too_many_files), do: "Too many files"
|
||||
defp upload_error_to_string(:not_accepted), do: "File type not accepted"
|
||||
defp upload_error_to_string(error), do: "Upload error: #{inspect(error)}"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user