Skip to content

Commit

Permalink
Merge pull request #149 from pulibrary/140-pagination
Browse files Browse the repository at this point in the history
Add pagination, sort-by, and date filter
  • Loading branch information
hackartisan authored Oct 24, 2024
2 parents 44bdb51 + 36c66f6 commit 83fea54
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 55 deletions.
40 changes: 35 additions & 5 deletions lib/dpul_collections/solr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,48 @@ defmodule DpulCollections.Solr do
response.body["response"]["numFound"]
end

def query(%{q: q}) do
params = [
q: q
@spec query(map()) :: map()
def query(search_state) do
solr_params = [
q: query_param(search_state),
"q.op": "AND",
sort: sort_param(search_state),
rows: search_state[:per_page],
start: pagination_offset(search_state)
]

{:ok, response} =
Req.get(
select_url(),
params: params
params: solr_params
)

response.body["response"]["docs"]
response.body["response"]
end

defp query_param(search_state) do
Enum.reject([search_state[:q], date_query(search_state)], &is_nil(&1))
|> Enum.join(" ")
end

defp date_query(%{date_from: nil, date_to: nil}), do: nil

defp date_query(%{date_from: date_from, date_to: date_to}) do
from = date_from || "*"
to = date_to || "*"
"years_is:[#{from} TO #{to}]"
end

defp sort_param(%{sort_by: sort_by}) do
case sort_by do
:relevance -> "score desc"
:date_desc -> "years_is desc"
:date_asc -> "years_is asc"
end
end

defp pagination_offset(%{page: page, per_page: per_page}) do
max(page - 1, 0) * per_page
end

def latest_document() do
Expand Down
8 changes: 3 additions & 5 deletions lib/dpul_collections_web/components/layouts/app.html.heex
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<.flash_group flash={@flash} />
<%= @inner_content %>
</main>
9 changes: 9 additions & 0 deletions lib/dpul_collections_web/live/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule DpulCollectionsWeb.Live.Helpers do
# Remove KV pairs with nil or empty string values
def clean_params(params) do
params
|> Enum.reject(fn {_, v} -> v == "" end)
|> Enum.reject(fn {_, v} -> is_nil(v) end)
|> Enum.into(%{})
end
end
3 changes: 2 additions & 1 deletion lib/dpul_collections_web/live/home_live.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule DpulCollectionsWeb.HomeLive do
use DpulCollectionsWeb, :live_view
alias DpulCollections.Solr
alias DpulCollectionsWeb.Live.Helpers

def mount(_params, _session, socket) do
socket =
Expand Down Expand Up @@ -32,7 +33,7 @@ defmodule DpulCollectionsWeb.HomeLive do
end

def handle_event("search", %{"q" => q}, socket) do
params = %{q: q}
params = %{q: q} |> Helpers.clean_params()
socket = push_navigate(socket, to: ~p"/search?#{params}")
{:noreply, socket}
end
Expand Down
182 changes: 162 additions & 20 deletions lib/dpul_collections_web/live/search_live.ex
Original file line number Diff line number Diff line change
@@ -1,49 +1,109 @@
defmodule DpulCollectionsWeb.SearchLive do
use DpulCollectionsWeb, :live_view
alias DpulCollections.Solr
alias DpulCollectionsWeb.Live.Helpers

defmodule Item do
defstruct [:id, :title]
defstruct [:id, :title, :date]
end

defmodule SearchState do
def from_params(params) do
%{
q: params["q"],
sort_by: valid_sort_by(params),
page: (params["page"] || "1") |> String.to_integer(),
per_page: (params["per_page"] || "10") |> String.to_integer(),
date_from: params["date_from"] || nil,
date_to: params["date_to"] || nil
}
end

defp valid_sort_by(%{"sort_by" => sort_by})
when sort_by in ["relevance", "date_desc", "date_asc"] do
String.to_existing_atom(sort_by)
end

defp valid_sort_by(_), do: :relevance
end

def mount(_params, _session, socket) do
{:ok, socket}
end

def handle_params(params, _uri, socket) do
filter = %{
q: valid_query(params)
}
search_state = SearchState.from_params(params)
solr_response = Solr.query(search_state)

items =
Solr.query(filter)
solr_response["docs"]
|> Enum.map(fn item ->
%Item{id: item["id"], title: item["title_ss"]}
%Item{id: item["id"], title: item["title_ss"], date: item["display_date_s"]}
end)

socket =
assign(socket,
filter: filter,
items: items
search_state: search_state,
items: items,
total_items: solr_response["numFound"]
)

{:noreply, socket}
end

def render(assigns) do
~H"""
<div class="grid grid-flow-row auto-rows-max gap-10">
<form phx-submit="search">
<div class="my-5 grid grid-flow-row auto-rows-max gap-10">
<form id="search-form" phx-submit="search">
<div class="grid grid-cols-4">
<input class="col-span-3" type="text" name="q" value={@filter.q} />
<button class="col-span-1" type="submit">
<input class="col-span-3" type="text" name="q" value={@search_state.q} />
<button class="col-span-1 font-bold uppercase" type="submit">
Search
</button>
</div>
</form>
<div class="grid grid-flow-row auto-rows-max gap-8">
<.search_item :for={item <- @items} item={item} />
<div id="date-filter" class="grid grid-cols-8 gap-4">
<label class="flex items-center font-bold uppercase" for="sort-by">filter by date: </label>
<input
class="col-span-1"
type="text"
placeholder="From"
form="search-form"
name="date-from"
value={@search_state.date_from}
/>
<input
class="col-span-1"
type="text"
placeholder="To"
form="search-form"
name="date-to"
value={@search_state.date_to}
/>
</div>
<form id="sort-form" phx-change="sort">
<div class="grid grid-cols-8">
<label class="col-span-1 flex items-center font-bold uppercase" for="sort-by">
sort by:
</label>
<select class="col-span-1" name="sort-by">
<%= Phoenix.HTML.Form.options_for_select(
["relevance", "date desc": "date_desc", "date asc": "date_asc"],
@search_state.sort_by
) %>
</select>
</div>
</form>
</div>
<div class="grid grid-flow-row auto-rows-max gap-8">
<.search_item :for={item <- @items} item={item} />
</div>
<div class="text-center bg-white max-w-5xl mx-auto text-lg py-8">
<.paginator
page={@search_state.page}
per_page={@search_state.per_page}
total_items={@total_items}
/>
</div>
"""
end
Expand All @@ -53,19 +113,101 @@ defmodule DpulCollectionsWeb.SearchLive do
def search_item(assigns) do
~H"""
<div class="item">
<div class="font-bold text-lg"><%= @item.title %></div>
<div class="underline text-lg"><%= @item.title %></div>
<div><%= @item.id %></div>
<div><%= @item.date %></div>
</div>
"""
end

def handle_event("search", %{"q" => q}, socket) do
params = %{q: q}
def paginator(assigns) do
~H"""
<div class="paginator">
<.link :if={@page > 1} id="paginator-previous" phx-click="paginate" phx-value-page={@page - 1}>
Previous
</.link>
<.link
:for={{page_number, current_page?} <- pages(@page, @per_page, @total_items)}
class={if current_page?, do: "active"}
phx-click="paginate"
phx-value-page={page_number}
>
<%= page_number %>
</.link>
<.link
:if={more_pages?(@page, @per_page, @total_items)}
id="paginator-next"
phx-click="paginate"
phx-value-page={@page + 1}
>
Next
</.link>
</div>
"""
end

def handle_event("search", params, socket) do
params =
%{
socket.assigns.search_state
| q: params["q"],
date_to: params["date-to"],
date_from: params["date-from"]
}
|> Helpers.clean_params()

socket = push_patch(socket, to: ~p"/search?#{params}")
{:noreply, socket}
end

defp valid_query(%{"q" => ""}), do: nil
defp valid_query(%{"q" => q}), do: q
defp valid_query(_), do: nil
def handle_event("sort", params, socket) do
params =
%{socket.assigns.search_state | sort_by: params["sort-by"]}
|> Helpers.clean_params()

socket = push_patch(socket, to: ~p"/search?#{params}")
{:noreply, socket}
end

def handle_event("paginate", %{"page" => page}, socket) when page != "..." do
params = %{socket.assigns.search_state | page: page} |> Helpers.clean_params()
socket = push_patch(socket, to: ~p"/search?#{params}")
{:noreply, socket}
end

def handle_event("paginate", _, socket) do
{:noreply, socket}
end

defp more_pages?(page, per_page, total_items) do
page * per_page < total_items
end

defp pages(page, per_page, total_items) do
page_count = ceil(total_items / per_page)
page_range = (page - 2)..(page + 2)

pages =
for page_number <- page_range,
page_number > 0 do
if page_number <= page_count do
current_page? = page_number == page
{page_number, current_page?}
end
end

# Add the prefix (1...) and postfix (...last_page)
# tail element to the paginator.
paginator_tail(:pre, 1, page_range) ++
pages ++
paginator_tail(:post, page_count, page_range)
end

defp paginator_tail(type, page, page_range) do
cond do
Enum.member?(page_range |> Enum.to_list(), page) -> []
type == :pre -> [{page, false}, {"...", false}]
type == :post -> [{"...", false}, {page, false}]
end
end
end
Loading

0 comments on commit 83fea54

Please sign in to comment.