lib/petal_components/pagination.ex

defmodule PetalComponents.Pagination do
  use Phoenix.Component
  alias PetalComponents.Heroicons
  import PetalComponents.Link

  # prop path, :string
  # prop class, :string
  # prop sibling_count, :integer
  # prop boundary_count, :integer
  # prop link_type, :string, options: ["a", "live_patch", "live_redirect"]


  @doc """
  In the `path` param you can specify :page as the place your page number will appear.
  e.g "/posts/:page" => "/posts/1"
  """

  def pagination(assigns) do
    assigns = assigns
      |> assign_new(:class, fn -> "" end)
      |> assign_new(:link_type, fn -> "a" end)
      |> assign_new(:sibling_count, fn -> 1 end)
      |> assign_new(:boundary_count, fn -> 1 end)
      |> assign_new(:path, fn -> "/:page" end)

    ~H"""
    <div class={"#{@class} flex"}>
      <ul class="inline-flex -space-x-px text-sm font-medium">
        <%= for item <- get_items(@total_pages, @current_page, @sibling_count, @boundary_count) do %>
          <%= if item.type == "previous" do %>
            <div>
              <.link type={@link_type} to={get_path(@path, item.number, @current_page)} class="mr-2 inline-flex items-center justify-center rounded leading-5 px-2.5 py-2 bg-white hover:bg-gray-50 border border-gray-200 text-gray-600 hover:text-gray-800">
                <Heroicons.Solid.chevron_left class="w-5 h-5 text-gray-600" />
              </.link>
            </div>
          <% end %>

          <%= if item.type == "page" do %>
            <li>
              <%= if item.number == @current_page do %>
                <span class={get_box_class(item, true)}><%= item.number %></span>
              <% else %>
                <.link type={@link_type} to={get_path(@path, item.number, @current_page)} class={get_box_class(item)}>
                  <%= item.number %>
                </.link>
              <% end %>
            </li>
          <% end %>

          <%= if item.type == "ellipsis" do %>
            <li>
              <span class="inline-flex items-center justify-center leading-5 px-3.5 py-2 bg-white border border-gray-200 text-gray-400">...</span>
            </li>
          <% end %>

          <%= if item.type == "next" do %>
            <div>
              <.link type={@link_type} to={get_path(@path, item.number, @current_page)} class="ml-2 inline-flex items-center justify-center rounded leading-5 px-2.5 py-2 bg-white hover:bg-gray-50 border border-gray-200 text-gray-600 hover:text-gray-800">
                <Heroicons.Solid.chevron_right class="w-5 h-5 text-gray-600" />
              </.link>
            </div>
          <% end %>
        <% end %>
      </ul>
    </div>
    """
  end


  defp get_items(total_pages, current_page, sibling_count, boundary_count) do
    start_pages = 1..min(boundary_count, total_pages) |> Enum.to_list()
    end_pages = max(total_pages - boundary_count + 1, boundary_count + 1)..total_pages |> Enum.to_list()

    siblings_start = max(
      min( current_page - sibling_count, total_pages - boundary_count - sibling_count * 2 - 1),
      boundary_count + 2
    )

    siblings_end = min(
      max(current_page + sibling_count, boundary_count + sibling_count * 2 + 2),
      (if length(end_pages) > 0, do: List.first(end_pages) - 2, else: total_pages - 1)
    )

    items = []

    # Previous button
    items = if current_page > 1, do: items ++ [%{type: "previous", number: current_page - 1}], else: items

    # Start pages
    items = Enum.reduce(start_pages, items, fn i, acc ->
      acc ++ [%{type: "page", number: i, first: i == 1}]
    end)

    # First ellipsis
    items = if siblings_start > boundary_count + 2, do: items ++ [%{type: "ellipsis"}], else: (if boundary_count + 1 < total_pages - boundary_count, do: items ++ [%{type: "page", number: boundary_count + 1}], else: items)

    # Siblings
    items = Enum.reduce(siblings_start..siblings_end, items, fn i, acc ->
      acc ++ [%{type: "page", number: i}]
    end)

    # Second ellipsis
    items = if siblings_end < total_pages - boundary_count - 1, do: items ++ [%{type: "ellipsis"}], else: (if total_pages - boundary_count > boundary_count, do: items ++ [%{type: "page", number: total_pages - boundary_count}], else: items)

    # End pages
    items = Enum.reduce(end_pages, items, fn i, acc ->
      acc ++ [%{type: "page", number: i, last: i == total_pages}]
    end)

    # Next button
    if current_page < total_pages, do: items ++ [%{type: "next", number: current_page + 1}], else: items
  end

  defp get_box_class(item, is_active \\ false) do
    base_classes = "inline-flex items-center justify-center leading-5 px-3.5 py-2 border border-gray-200 hover:bg-gray-50 border border-gray-200 text-gray-600 hover:text-gray-800"
    active_classes = if is_active, do: "bg-gray-100 hover:bg-gray-100 text-gray-800", else: "text-gray-600 hover:text-gray-800"
    rounded_classes = cond do
      item[:first] ->
        "rounded-l "
      item[:last] ->
        "rounded-r"
      true ->
        ""
    end

    Enum.join([base_classes, active_classes, rounded_classes], " ")
  end

  defp get_path(path, "previous", current_page) do
    String.replace(path, ":page", Integer.to_string(current_page - 1))
  end

  defp get_path(path, "next", current_page) do
    String.replace(path, ":page", Integer.to_string(current_page + 1))
  end

  defp get_path(path, page_number, _current_page) do
    String.replace(path, ":page", Integer.to_string(page_number))
  end
end