defmodule PhiaUi.Components.AvatarUpload do @moduledoc """ Circular or square profile photo uploader with overlay edit icon. Provides `avatar_upload/1` — a compact click-to-upload zone designed for profile pictures and avatars. When an existing photo URL is provided, it is shown as the background image with a semi-transparent overlay containing a camera icon on hover. When a new file is selected via LiveView's upload system, the `live_img_preview/1` helper renders an instant local preview. ## Requirements The parent LiveView must configure the upload with `allow_upload/3`: def mount(_params, _session, socket) do {:ok, socket |> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png .webp .gif), max_entries: 1, max_file_size: 5_242_880 )} end ## Basic usage <%!-- Profile photo upload --%> <.avatar_upload upload={@uploads.avatar} current_url={@user.avatar_url} on_cancel="cancel_avatar" /> ## With shape and size variants <.avatar_upload upload={@uploads.avatar} current_url={@user.avatar_url} shape="square" size="lg" /> ## Handling cancel def handle_event("cancel_avatar", %{"ref" => ref}, socket) do {:noreply, cancel_upload(socket, :avatar, ref)} end """ use Phoenix.Component import PhiaUi.ClassMerger, only: [cn: 1] # --------------------------------------------------------------------------- # avatar_upload/1 # --------------------------------------------------------------------------- attr(:upload, :any, required: true, doc: "A `Phoenix.LiveView.UploadConfig` struct from `@uploads.field_name`." ) attr(:current_url, :string, default: nil, doc: "URL of the currently saved photo. Shown as background until a new file is selected." ) attr(:shape, :string, default: "circle", values: ~w(circle square), doc: "Shape of the upload zone — `circle` (rounded-full) or `square` (rounded-lg)." ) attr(:size, :string, default: "md", values: ~w(sm md lg xl), doc: "Controls the width/height of the upload zone." ) attr(:on_cancel, :string, default: "cancel_upload", doc: "LiveView event name fired when the remove (×) button is clicked." ) attr(:class, :string, default: nil, doc: "Additional CSS classes applied to the outer wrapper." ) @doc """ Renders a profile photo upload zone with optional existing image preview. Layout: 1. If entries exist → `live_img_preview/1` of the first pending entry + remove button 2. If no entries → click-to-upload zone showing `current_url` (or a placeholder icon) 3. In both cases, a camera overlay icon is shown on hover ## Examples <.avatar_upload upload={@uploads.avatar} current_url={@user.photo_url} /> <.avatar_upload upload={@uploads.avatar} shape="square" size="lg" /> """ def avatar_upload(assigns) do entries = Map.get(assigns.upload, :entries, []) assigns = assign(assigns, :entries, entries) ~H"""
<%= if @entries != [] do %> <%!-- Preview of the newly selected file --%>
<.live_img_preview entry={List.first(@entries)} class="w-full h-full object-cover" /> <%!-- Remove button --%>
<% else %> <%!-- Click-to-upload drop zone --%> <% end %> <%!-- Per-upload validation errors --%> <%= for err <- get_upload_errors(@upload) do %>

{humanize_error(err)}

<% end %>
""" end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp size_class("sm"), do: "w-16 h-16" defp size_class("md"), do: "w-24 h-24" defp size_class("lg"), do: "w-32 h-32" defp size_class("xl"), do: "w-40 h-40" defp size_class(_), do: "w-24 h-24" defp shape_class("circle"), do: "rounded-full" defp shape_class("square"), do: "rounded-lg" defp shape_class(_), do: "rounded-full" defp camera_icon_size("sm"), do: "h-4 w-4" defp camera_icon_size("md"), do: "h-6 w-6" defp camera_icon_size("lg"), do: "h-7 w-7" defp camera_icon_size("xl"), do: "h-8 w-8" defp camera_icon_size(_), do: "h-6 w-6" defp camera_label_size("sm"), do: "hidden" defp camera_label_size("md"), do: "text-[10px]" defp camera_label_size("lg"), do: "text-xs" defp camera_label_size("xl"), do: "text-sm" defp camera_label_size(_), do: "text-[10px]" defp placeholder_icon_size("sm"), do: "h-7 w-7" defp placeholder_icon_size("md"), do: "h-10 w-10" defp placeholder_icon_size("lg"), do: "h-12 w-12" defp placeholder_icon_size("xl"), do: "h-16 w-16" defp placeholder_icon_size(_), do: "h-10 w-10" defp get_upload_errors(upload), do: Map.get(upload, :errors, []) defp humanize_error(:too_large), do: "File too large" defp humanize_error(:not_accepted), do: "Invalid file type" defp humanize_error(:too_many_files), do: "Too many files" defp humanize_error(other), do: to_string(other) end