lib/phoenix/ui/components/avatar.ex

defmodule Phoenix.UI.Components.Avatar do
  @moduledoc """
  Provides avatar component.
  """
  import Phoenix.UI.Components.Heroicon

  use Phoenix.UI, :component

  attr(:border, :boolean, default: false)
  attr(:color, :string, default: "slate", values: Theme.colors())
  attr(:element, :string, default: "div")
  attr(:extend_class, :string)
  attr(:rest, :global)
  attr(:size, :string, default: "md", values: ["xs", "sm", "md", "lg", "xl"])
  attr(:variant, :string, default: "circular", values: ["circular", "rounded"])

  slot(:inner_block)

  @doc """
  Renders avatar component.

  ## Examples

      ```
      <.avatar src={@src} />
      ```

  """
  @spec avatar(Socket.assigns()) :: Rendered.t()
  def avatar(assigns) do
    assigns
    |> assign_class(~w(
      avatar relative overflow-hidden font-semibold
      inline-flex items-center justify-center
      #{classes(:border, assigns)}
      #{classes(:color, assigns)}
      #{classes(:size, assigns)}
      #{classes(:variant, assigns)}
    ))
    |> generate_markup()
  end

  ### Markup ##########################

  defp generate_markup(%{rest: %{src: src}} = assigns) when not is_nil(src) do
    ~H"""
    <img class={@class} {@rest} />
    """
  end

  defp generate_markup(%{inner_block: [], rest: %{alt: alt}} = assigns) when not is_nil(alt) do
    ~H"""
    <.dynamic_tag class={@class} name={@element} {@rest}>
      <%= build_initials(@rest[:alt]) %>
    </.dynamic_tag>
    """
  end

  defp generate_markup(%{inner_block: []} = assigns) do
    ~H"""
    <.dynamic_tag class={@class} name={@element} {@rest}>
      <.heroicon
        extend_class="absolute scale-125 top-[15%]"
        name="user"
        size={icon_size_mapping(@size)}
        variant="mini"
      />
    </.dynamic_tag>
    """
  end

  defp generate_markup(assigns) do
    ~H"""
    <.dynamic_tag class={@class} name={@element} {@rest}>
      <%= render_slot(@inner_block) %>
    </.dynamic_tag>
    """
  end

  ### CSS Classes ##########################

  # Border
  defp classes(:border, %{border: true}), do: "ring-2 ring-white"

  # Color
  defp classes(:color, %{color: color}) do
    "bg-#{color}-300 text-slate-600 dark:bg-#{color}-600 dark:text-slate-200"
  end

  # Size
  defp classes(:size, %{size: "xs"}), do: "w-6 h-6 text-xs"
  defp classes(:size, %{size: "sm"}), do: "w-8 h-8 text-sm"
  defp classes(:size, %{size: "md"}), do: "w-10 h-10 text-base"
  defp classes(:size, %{size: "lg"}), do: "w-12 h-12 text-lg"
  defp classes(:size, %{size: "xl"}), do: "w-14 h-14 text-xl"
  defp classes(:size, %{size: val}), do: "w-[#{val}rem] h-[#{val}rem]"

  # Variant
  defp classes(:variant, %{variant: "circular"}), do: "rounded-full"
  defp classes(:variant, %{variant: "rounded"}), do: "rounded"

  defp classes(_rule_group, _assigns), do: nil

  ### Misc. Helpers ##########################

  defp build_initials(name) do
    name
    |> String.split(" ")
    |> case do
      [part1 | [part2 | _]] -> String.slice(part1, 0, 1) <> String.slice(part2, 0, 1)
      [part] -> String.slice(part, 0, 1)
    end
    |> String.upcase()
  end

  defp icon_size_mapping("xs"), do: 1.5
  defp icon_size_mapping("sm"), do: 2
  defp icon_size_mapping("md"), do: 2.5
  defp icon_size_mapping("lg"), do: 3
  defp icon_size_mapping("xl"), do: 3.5
  defp icon_size_mapping(size), do: size
end