add pagination across all admin and shop views
All checks were successful
deploy / deploy (push) Successful in 1m38s
All checks were successful
deploy / deploy (push) Successful in 1m38s
URL-based offset pagination with ?page=N for bookmarkable pages. Admin views use push_patch, shop collection uses navigate links. Responsive on mobile with horizontal-scroll tables and stacking pagination controls. Includes dev seed script for testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -333,34 +333,36 @@ defmodule BerrypodWeb.CoreComponents do
|
||||
end
|
||||
|
||||
~H"""
|
||||
<table class="admin-table admin-table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :for={col <- @col}>{col[:label]}</th>
|
||||
<th :if={@action != []}>
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={@row_click && "hover:cursor-pointer"}
|
||||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
<%= for action <- @action do %>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table admin-table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :for={col <- @col}>{col[:label]}</th>
|
||||
<th :if={@action != []}>
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={@row_click && "hover:cursor-pointer"}
|
||||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
<%= for action <- @action do %>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -622,4 +624,88 @@ defmodule BerrypodWeb.CoreComponents do
|
||||
|> JS.exec("close()", to: "##{id}")
|
||||
|> JS.pop_focus()
|
||||
end
|
||||
|
||||
# ── Pagination ───────────────────────────────────────────────────
|
||||
|
||||
@doc """
|
||||
Renders pagination controls for admin lists.
|
||||
|
||||
Hidden when there's only one page. Shows "Showing X-Y of Z" on the left,
|
||||
page number buttons with ellipsis on the right.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `page` - Required. A `%Berrypod.Pagination{}` struct.
|
||||
* `patch` - Required. Base URL path for pagination links (e.g. "/admin/products").
|
||||
* `params` - Extra query params to preserve (e.g. %{"tab" => "broken"}). Default `%{}`.
|
||||
"""
|
||||
attr :page, Berrypod.Pagination, required: true
|
||||
attr :patch, :string, required: true
|
||||
attr :params, :map, default: %{}
|
||||
|
||||
def admin_pagination(assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:showing, Berrypod.Pagination.showing_text(assigns.page))
|
||||
|> assign(:numbers, Berrypod.Pagination.page_numbers(assigns.page))
|
||||
|
||||
~H"""
|
||||
<nav
|
||||
:if={@page.total_pages > 1}
|
||||
aria-label="Pagination"
|
||||
class="admin-pagination"
|
||||
>
|
||||
<p class="admin-pagination-showing">{@showing}</p>
|
||||
|
||||
<div class="admin-pagination-buttons">
|
||||
<.link
|
||||
patch={admin_page_url(@patch, @page.page - 1, @params)}
|
||||
class={["admin-btn admin-btn-sm admin-btn-ghost", @page.page == 1 && "admin-btn-disabled"]}
|
||||
aria-label="Previous page"
|
||||
aria-disabled={@page.page == 1 && "true"}
|
||||
tabindex={@page.page == 1 && "-1"}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="size-4" />
|
||||
</.link>
|
||||
|
||||
<%= for item <- @numbers do %>
|
||||
<%= case item do %>
|
||||
<% :ellipsis -> %>
|
||||
<span class="admin-pagination-ellipsis" aria-hidden="true">…</span>
|
||||
<% n -> %>
|
||||
<.link
|
||||
patch={admin_page_url(@patch, n, @params)}
|
||||
aria-label={"Page #{n}"}
|
||||
aria-current={n == @page.page && "page"}
|
||||
class={[
|
||||
"admin-btn admin-btn-sm",
|
||||
if(n == @page.page, do: "admin-btn-primary", else: "admin-btn-ghost")
|
||||
]}
|
||||
>
|
||||
{n}
|
||||
</.link>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<.link
|
||||
patch={admin_page_url(@patch, @page.page + 1, @params)}
|
||||
class={[
|
||||
"admin-btn admin-btn-sm admin-btn-ghost",
|
||||
@page.page == @page.total_pages && "admin-btn-disabled"
|
||||
]}
|
||||
aria-label="Next page"
|
||||
aria-disabled={@page.page == @page.total_pages && "true"}
|
||||
tabindex={@page.page == @page.total_pages && "-1"}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="size-4" />
|
||||
</.link>
|
||||
</div>
|
||||
</nav>
|
||||
"""
|
||||
end
|
||||
|
||||
defp admin_page_url(base, page, params) do
|
||||
query = if page > 1, do: Map.put(params, "page", to_string(page)), else: params
|
||||
if query == %{}, do: base, else: base <> "?" <> URI.encode_query(query)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user