Skip to content

Commit d22921a

Browse files
feat: pagination
1 parent f3ab4cc commit d22921a

4 files changed

Lines changed: 170 additions & 27 deletions

File tree

lib/yearbook/entries.ex

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,33 +87,31 @@ defmodule Yearbook.Entries do
8787
end
8888

8989
@doc """
90-
Returns the list of accepted entries.
91-
92-
## Options
93-
* `:filters` - A map of filters to apply to the query.
94-
Ex: %{"masters" => "true", "year" => "2026"}
90+
Returns a paginated map of accepted entries based on filters and page.
9591
"""
96-
def list_accepted_entries(filters \\ %{}) do
92+
def list_accepted_entries_pagination(params \\ %{}) do
9793
query = from e in Entry, where: e.status == :accepted
9894

9995
query =
100-
case Map.get(filters, "masters") do
96+
case Map.get(params, "masters") do
10197
"true" -> from e in query, where: e.masters == true
10298
"false" -> from e in query, where: e.masters == false
10399
_ -> query
104100
end
105101

106102
query =
107-
case Map.get(filters, "year") do
103+
case Map.get(params, "year") do
108104
year_str when year_str in [nil, ""] ->
109105
query
110106

111107
year_str ->
112-
year_int = String.to_integer(year_str)
108+
year_int = if is_binary(year_str), do: String.to_integer(year_str), else: year_str
113109
from e in query, where: e.year == ^year_int
114110
end
115111

116-
Repo.all(from e in query, order_by: [asc: e.name])
112+
query = from e in query, order_by: [asc: e.name]
113+
114+
paginate(query, params)
117115
end
118116

119117
@doc """
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
defmodule YearbookWeb.Components.Pagination do
2+
@moduledoc """
3+
UI components for paginated navigation.
4+
"""
5+
use Phoenix.Component
6+
7+
alias Plug.Conn.Query
8+
use Gettext, backend: YearbookWeb.Gettext
9+
10+
attr :meta, :any, required: true
11+
attr :params, :map, required: true
12+
13+
def pagination(assigns) do
14+
~H"""
15+
<nav class="flex items-center justify-between pt-6 pb-2" aria-label="Pagination">
16+
<span class="text-sm text-gray-500">
17+
{gettext("Showing")}
18+
<span class="font-bold text-gray-700">
19+
{@meta.current_offset + 1}-{@meta.next_offset || @meta.total_count}
20+
</span>
21+
{gettext("of")}
22+
<span class="font-bold text-gray-700">
23+
{@meta.total_count}
24+
</span>
25+
</span>
26+
27+
<ul class="inline-flex items-center -space-x-px text-sm rounded-lg shadow-sm">
28+
<.pagination_button
29+
text={gettext("Previous")}
30+
left_corner={true}
31+
disabled={!@meta.has_previous_page?}
32+
page={@meta.previous_page}
33+
params={@params}
34+
/>
35+
36+
<%= if max(1, @meta.current_page - 2) != 1 do %>
37+
<.pagination_button page={1} params={@params} />
38+
<li
39+
:if={max(1, @meta.current_page - 2) > 2}
40+
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-400 bg-white border border-gray-200 cursor-default"
41+
>
42+
...
43+
</li>
44+
<% end %>
45+
46+
<%= for page <- max(1, @meta.current_page - 2)..max(min(@meta.total_pages, @meta.current_page + 2), 1) do %>
47+
<.pagination_button page={page} params={@params} is_current={@meta.current_page == page} />
48+
<% end %>
49+
50+
<%= if min(@meta.total_pages, @meta.current_page + 2) != @meta.total_pages do %>
51+
<li
52+
:if={min(@meta.total_pages, @meta.current_page + 2) < @meta.total_pages - 1}
53+
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-400 bg-white border border-gray-200 cursor-default"
54+
>
55+
...
56+
</li>
57+
<.pagination_button page={@meta.total_pages} params={@params} />
58+
<% end %>
59+
60+
<.pagination_button
61+
text={gettext("Next")}
62+
right_corner={true}
63+
disabled={!@meta.has_next_page?}
64+
page={@meta.next_page}
65+
params={@params}
66+
/>
67+
</ul>
68+
</nav>
69+
"""
70+
end
71+
72+
attr :text, :string, default: ""
73+
attr :disabled, :boolean, default: false
74+
attr :left_corner, :boolean, default: false
75+
attr :right_corner, :boolean, default: false
76+
attr :is_current, :boolean, default: false
77+
attr :page, :integer
78+
attr :params, :map
79+
80+
defp pagination_button(assigns) do
81+
if assigns.disabled do
82+
~H"""
83+
<li>
84+
<span class={[
85+
"flex items-center justify-center px-3 h-8 leading-tight text-gray-400 bg-white border border-gray-200 cursor-not-allowed",
86+
@right_corner && "rounded-r-lg",
87+
@left_corner && "rounded-l-lg"
88+
]}>
89+
{if @text == "", do: @page, else: @text}
90+
</span>
91+
</li>
92+
"""
93+
else
94+
~H"""
95+
<li>
96+
<.link
97+
patch={build_query("page", @page, @params)}
98+
class={[
99+
"flex items-center justify-center px-3 h-8 leading-tight border border-gray-200 transition-colors",
100+
@right_corner && "rounded-r-lg",
101+
@left_corner && "rounded-l-lg",
102+
!@is_current && "text-gray-500 bg-white hover:bg-gray-50 hover:text-gray-700",
103+
@is_current && "text-gray-800 bg-gray-50 font-medium z-10"
104+
]}
105+
>
106+
{if @text == "", do: @page, else: @text}
107+
</.link>
108+
</li>
109+
"""
110+
end
111+
end
112+
113+
defp build_query(key, value, params) do
114+
query = Map.put(params, key, value)
115+
"?#{Query.encode(query)}"
116+
end
117+
end

lib/yearbook_web/live/home_live/index.ex

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,40 @@ defmodule YearbookWeb.HomeLive.Index do
55
use YearbookWeb, :landing_view
66
alias Yearbook.Entries
77
import YearbookWeb.Components.EntryCard
8+
import YearbookWeb.Components.Pagination
89

9-
def mount(_params, _session, socket) do
10-
entries = Entries.list_accepted_entries(%{"masters" => "", "year" => ""})
10+
@page_size 10
1111

12+
def mount(_params, _session, socket) do
1213
{:ok,
1314
socket
1415
|> assign(page_title: "Home | Yearbook")
15-
|> assign(entries: entries)
16-
|> assign(total_entries: Enum.count(entries))
17-
|> assign(filters: %{"masters" => "", "year" => ""})}
16+
|> assign(filters: %{"masters" => "", "year" => ""})
17+
|> assign(page_size: @page_size)}
1818
end
1919

20-
def handle_params(_params, _url, socket) do
21-
{:noreply, socket}
22-
end
20+
def handle_params(params, _url, socket) do
21+
page = params["page"] || "1"
2322

24-
def handle_event("filter_changed", params, socket) do
25-
entries = Entries.list_accepted_entries(params)
23+
new_filters = Map.take(params, ["masters", "year"])
24+
current_filters = Map.merge(socket.assigns.filters, new_filters)
25+
26+
results =
27+
Entries.list_accepted_entries_pagination(Map.merge(current_filters, %{"page" => page}))
2628

2729
{:noreply,
2830
socket
29-
|> assign(entries: entries)
30-
|> assign(total_entries: Enum.count(entries))
31-
|> assign(filters: params)}
31+
|> assign(entries: results.entries)
32+
|> assign(filters: current_filters)
33+
|> assign(params: params)
34+
|> assign(current_page_num: results.current_page)
35+
|> assign(total_pages: results.total_pages)
36+
|> assign(total_count: results.total_count)}
37+
end
38+
39+
def handle_event("filter_changed", params, socket) do
40+
new_params = Map.put(params, "page", 1)
41+
{:noreply, push_patch(socket, to: ~p"/?#{new_params}")}
3242
end
3343

3444
defp list_academic_years do

lib/yearbook_web/live/home_live/index.html.heex

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="p-4 md:p-10 w-full min-h-screen bg-app-bg">
22
<div class="max-w-7xl mx-auto">
3-
<div class="mb-12">
3+
<div class="mb-8">
44
<div class="flex items-center gap-4">
55
<h1 class="text-4xl md:text-5xl font-bold text-[#1A1A1A] tracking-tight">
66
Yearbook
@@ -57,7 +57,7 @@
5757
</div>
5858

5959
<div class="flex items-center gap-8 mt-8">
60-
<div class="flex gap-8">
60+
<div class="flex gap-7">
6161
<button
6262
phx-click="filter_changed"
6363
phx-value-masters=""
@@ -104,7 +104,7 @@
104104
<%= if assigns[:current_scope] && @current_scope.user do %>
105105
<.link
106106
patch={~p"/entries/new"}
107-
class="flex items-center justify-center gap-2 px-3 py-1.5 bg-white border border-gray-200 rounded-lg text-xs font-bold uppercase tracking-wider text-gray-600 hover:bg-gray-50 hover:border-orange-200 hover:text-orange-600 transition-all shadow-sm min-w-[40px]"
107+
class="flex items-center justify-center gap-2 px-3 py-1.5 bg-white border border-gray-200 rounded-lg text-xs font-bold uppercase tracking-wider text-gray-600 hover:bg-gray-50 hover:border-orange-200 hover:text-orange-600 transition-all shadow-sm min-w-10"
108108
>
109109
<.icon name="hero-plus" class="size-4 " />
110110

@@ -129,10 +129,28 @@
129129
</p>
130130
</div>
131131

132-
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-8 gap-y-12">
132+
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-8 gap-y-10">
133133
<%= for entry <- @entries do %>
134134
<.entry_card name={entry.name} text={entry.text} src={entry.photo} />
135135
<% end %>
136136
</div>
137+
<div :if={@total_pages > 1} class="mt-16 border-t border-gray-100">
138+
<.pagination
139+
params={@params}
140+
meta={
141+
%{
142+
total_pages: @total_pages,
143+
current_page: @current_page_num,
144+
total_count: @total_count,
145+
current_offset: (@current_page_num - 1) * 10,
146+
next_offset: min(@current_page_num * 10, @total_count),
147+
previous_page: @current_page_num - 1,
148+
next_page: @current_page_num + 1,
149+
has_previous_page?: @current_page_num > 1,
150+
has_next_page?: @current_page_num < @total_pages
151+
}
152+
}
153+
/>
154+
</div>
137155
</div>
138156
</div>

0 commit comments

Comments
 (0)