lib/fermo/pagination.ex

defmodule Fermo.Pagination do
  @enforce_keys [:items, :page, :total_items, :base, :suffix]
  defstruct items: nil,
    page: nil,
    per_page: 10,
    total_items: nil,
    base: nil,
    suffix: "?page=:page",
    first: nil

  @type t() :: %__MODULE__{
    items: Array.t(),
    page: integer(),
    per_page: integer(),
    total_items: integer(),
    base: String.t(),
    suffix: String.t(),
    first: String.t()
  }

  @callback paginate(map(), String.t(), map(), map(), function()) :: map()
  def paginate(config, template, options \\ %{}, context \\ %{}, fun \\ nil) do
    base = options.base
    items = options.items
    per_page = options[:per_page] || 20
    suffix = options[:suffix] || "pages/:page/index.html"
    first = options[:first]
    total_items = length(items)

    paginated = Stream.chunk_every(items, per_page, per_page, [])
    |> Stream.with_index
    |> Enum.map(fn ({chunk, i}) ->
      # index is 1 based
      index = i + 1
      pagination = %__MODULE__{
        items: chunk,
        total_items: total_items,
        page: index,
        per_page: per_page,
        base: base,
        suffix: suffix,
        first: first
      }

      with_pagination = Map.put(context, :pagination, pagination)
      prms = if fun do
        fun.(with_pagination, index)
      else
        with_pagination
      end

      Fermo.Config.page_from(template, page_path(pagination), prms)
    end)

    pages = Map.get(config, :pages, [])
    put_in(config, [:pages], pages ++ paginated)
  end

  def total_pages(%__MODULE__{} = pagination) do
    round((pagination.total_items - 1) / pagination.per_page + 1)
  end

  def paginatable?(%__MODULE__{} = pagination) do
    pagination.page > 1 || pagination.page < total_pages(pagination)
  end

  def prev_page(%__MODULE__{} = pagination) do
    to_page(pagination, pagination.page - 1)
  end

  def next_page(%__MODULE__{} = pagination) do
    to_page(pagination, pagination.page + 1)
  end

  def to_page(%__MODULE__{} = pagination, page) do
    page_path(%__MODULE__{pagination | page: page, items: []})
  end

  def page_path(%__MODULE__{} = pagination) do
    cond do
      pagination.page < 1 -> nil
      pagination.page == 1 && pagination.first ->
        pagination.base <> pagination.first
      pagination.page > total_pages(pagination) -> nil
      true ->
        path = Regex.replace(
          ~r/:([\w_]+)/,
          pagination.suffix,
          fn _, name ->
            num = Map.get(pagination, String.to_atom(name))
            Integer.to_charlist(num) |> List.to_string
          end
        )
        pagination.base <> path
    end
  end
end