lib/sanity/components/image.ex

defmodule Sanity.Components.Image do
  @moduledoc """
  For rendering a [Sanity image asset](https://www.sanity.io/docs/assets).

  ## Examples

      use Phoenix.Component

      # ...

      # Example of image asset returned by Sanity CMS API
      assigns = %{
        image: %{
          _id: "image-da994d9e87efb226111cb83dbbab832d45b1365e-1500x750-jpg",
          _type: "sanity.imageAsset",
          metadata: %{
            dimensions: %{height: 750, width: 1500},
            palette: %{dominant: %{background: "#0844c5"}}
          },
          mime_type: "image/jpeg",
          url:
            "https://cdn.sanity.io/images/csbsxnjq/production/da994d9e87efb226111cb83dbbab832d45b1365e-1500x750.jpg"
        }
      }

      ~H"<Sanity.Components.Image.sanity_image image={@image} />"
  """

  use Phoenix.Component

  @breakpoints [320, 768, 1024, 1600, 2048]

  @projection """
  {
    _id,
    _type,
    metadata {
      dimensions { height, width },
      palette {
        dominant { background }
      },
    },
    mimeType,
    url,
  }
  """

  @doc """
  Returns a GROQ projection for a Sanity image.
  """
  def projection, do: @projection

  @doc """
  Renders a responsive sanity image.

  The `src` and `srcset` attributes will be automatically set. Sanity CMS will [take care of
  resizing the images and serving WebP images to supported
  browsers](https://www.sanity.io/docs/image-urls).

  The `width` and `height` attributes will be automatically set to the dimensions of the image.
  This ensures that on [modern
  browsers](https://caniuse.com/mdn-html_elements_img_aspect_ratio_computed_from_attributes) the
  image will have the correct aspect ratio before the image loads. This avoids [layout
  shift](https://web.dev/cls/).

  See module doc for example.
  """

  attr :image, :any, required: true
  attr :height, :integer
  attr :width, :integer
  attr :style, :string
  attr :sizes, :string, default: "100vw"
  attr :rest, :global

  def sanity_image(assigns) do
    metadata = assigns.image.metadata

    assigns =
      assigns
      |> assign_new(:height, fn -> metadata.dimensions.height end)
      |> assign_new(:width, fn -> metadata.dimensions.width end)
      |> assign_new(:style, fn -> "--sanity-image-bg: #{metadata.palette.dominant.background}" end)

    ~H"""
    <img height={@height} width={@width} style={@style} sizes={@sizes} src={src(@image)} srcset={srcset(@image)} {@rest} />
    """
  end

  defp src(%{mime_type: "image/svg+xml", url: url}), do: url
  defp src(%{mime_type: _, url: url}), do: image_url(url, 1024)

  defp srcset(%{mime_type: "image/svg+xml"}), do: nil

  defp srcset(%{mime_type: _, url: url}) do
    {breakpoints, [last_breakpoint]} = Enum.split(@breakpoints, -1)

    breakpoints
    |> Enum.map(fn w -> "#{image_url(url, w)} #{w}w" end)
    |> Enum.concat([image_url(url, last_breakpoint)])
    |> Enum.join(",")
  end

  defp image_url(url, size) when is_binary(url) and is_integer(size) do
    params = %{auto: "format", fit: "min", w: size}

    "#{url}?#{URI.encode_query(params)}"
  end
end