add pagination across all admin and shop views
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:
jamey
2026-03-01 09:42:34 +00:00
parent 7f6fd012a5
commit 3480b326a9
21 changed files with 1485 additions and 211 deletions

View File

@@ -5,20 +5,18 @@ defmodule BerrypodWeb.Admin.Media do
@impl true
def mount(_params, _session, socket) do
images = Media.list_images()
socket =
socket
|> assign(:page_title, "Media")
|> assign(:filter_type, nil)
|> assign(:filter_search, "")
|> assign(:filter_orphans, false)
|> assign(:pagination, nil)
|> assign(:selected_image, nil)
|> assign(:selected_usages, [])
|> assign(:edit_form, nil)
|> assign(:upload_alt, "")
|> assign(:confirm_delete, false)
|> stream(:images, images)
|> allow_upload(:media_upload,
accept: ~w(.png .jpg .jpeg .webp .svg .gif),
max_entries: 1,
@@ -30,6 +28,20 @@ defmodule BerrypodWeb.Admin.Media do
{:ok, socket}
end
@impl true
def handle_params(params, _uri, socket) do
page_num = Berrypod.Pagination.parse_page(params)
opts = image_filter_opts(socket)
page = Media.list_images_paginated([page: page_num] ++ opts)
socket =
socket
|> assign(:pagination, page)
|> stream(:images, page.items, reset: true)
{:noreply, socket}
end
defp handle_progress(:media_upload, entry, socket) do
if entry.done? do
alt = socket.assigns.upload_alt
@@ -60,15 +72,25 @@ defmodule BerrypodWeb.Admin.Media do
@impl true
def handle_event("filter_type", %{"type" => type}, socket) do
type = if type == "", do: nil, else: type
{:noreply, reload_images(assign(socket, :filter_type, type))}
{:noreply,
socket
|> assign(:filter_type, type)
|> reload_images()}
end
def handle_event("filter_search", %{"value" => value}, socket) do
{:noreply, reload_images(assign(socket, :filter_search, value))}
{:noreply,
socket
|> assign(:filter_search, value)
|> reload_images()}
end
def handle_event("toggle_orphans", _params, socket) do
{:noreply, reload_images(assign(socket, :filter_orphans, !socket.assigns.filter_orphans))}
{:noreply,
socket
|> assign(:filter_orphans, !socket.assigns.filter_orphans)
|> reload_images()}
end
def handle_event("select_image", %{"id" => id}, socket) do
@@ -159,26 +181,29 @@ defmodule BerrypodWeb.Admin.Media do
# ── Private helpers ──────────────────────────────────────────────
defp image_filter_opts(socket) do
[
type: socket.assigns.filter_type,
search: if(socket.assigns.filter_search != "", do: socket.assigns.filter_search),
tag: nil
]
|> Enum.reject(fn {_, v} -> is_nil(v) end)
end
defp reload_images(socket) do
opts =
[
type: socket.assigns.filter_type,
search: if(socket.assigns.filter_search != "", do: socket.assigns.filter_search),
tag: nil
]
|> Enum.reject(fn {_, v} -> is_nil(v) end)
if socket.assigns.filter_orphans do
# Orphan mode: load all, filter in Elixir (no pagination)
opts = image_filter_opts(socket)
images = Media.list_images(opts)
used = Media.used_image_ids()
orphans = Enum.reject(images, &MapSet.member?(used, &1.id))
images = Media.list_images(opts)
images =
if socket.assigns.filter_orphans do
used = Media.used_image_ids()
Enum.reject(images, &MapSet.member?(used, &1.id))
else
images
end
stream(socket, :images, images, reset: true)
socket
|> assign(:pagination, nil)
|> stream(:images, orphans, reset: true)
else
push_patch(socket, to: ~p"/admin/media")
end
end
defp format_file_size(nil), do: ""
@@ -284,49 +309,53 @@ defmodule BerrypodWeb.Admin.Media do
</div>
<div class="media-main">
<%!-- image grid --%>
<div id="media-grid" phx-update="stream" class="media-grid">
<div
:for={{dom_id, image} <- @streams.images}
id={dom_id}
phx-click="select_image"
phx-value-id={image.id}
class={[
"media-card",
@selected_image && @selected_image.id == image.id && "media-card-selected"
]}
>
<div class="media-card-thumb">
<%= if image.is_svg do %>
<div class="media-card-svg-placeholder">
<.icon name="hero-code-bracket" class="size-8" />
<span>SVG</span>
</div>
<% else %>
<%= if thumb = image_thumbnail_url(image) do %>
<img src={thumb} alt={image.alt || image.filename} loading="lazy" />
<% else %>
<%!-- image grid + pagination --%>
<div class="media-grid-wrapper">
<div id="media-grid" phx-update="stream" class="media-grid">
<div
:for={{dom_id, image} <- @streams.images}
id={dom_id}
phx-click="select_image"
phx-value-id={image.id}
class={[
"media-card",
@selected_image && @selected_image.id == image.id && "media-card-selected"
]}
>
<div class="media-card-thumb">
<%= if image.is_svg do %>
<div class="media-card-svg-placeholder">
<.icon name="hero-photo" class="size-8" />
<.icon name="hero-code-bracket" class="size-8" />
<span>SVG</span>
</div>
<% else %>
<%= if thumb = image_thumbnail_url(image) do %>
<img src={thumb} alt={image.alt || image.filename} loading="lazy" />
<% else %>
<div class="media-card-svg-placeholder">
<.icon name="hero-photo" class="size-8" />
</div>
<% end %>
<% end %>
<% end %>
</div>
<div class="media-card-info">
<span class="media-card-filename" title={image.filename}>{image.filename}</span>
<div class="media-card-meta">
<span class={type_badge_class(image.image_type)}>{image.image_type}</span>
<span class="text-xs">{format_file_size(image.file_size)}</span>
</div>
<span
:if={!image.alt || image.alt == ""}
class="media-card-no-alt"
title="Missing alt text"
>
<.icon name="hero-exclamation-triangle" class="size-3" /> No alt text
</span>
<div class="media-card-info">
<span class="media-card-filename" title={image.filename}>{image.filename}</span>
<div class="media-card-meta">
<span class={type_badge_class(image.image_type)}>{image.image_type}</span>
<span class="text-xs">{format_file_size(image.file_size)}</span>
</div>
<span
:if={!image.alt || image.alt == ""}
class="media-card-no-alt"
title="Missing alt text"
>
<.icon name="hero-exclamation-triangle" class="size-3" /> No alt text
</span>
</div>
</div>
</div>
<.admin_pagination :if={@pagination} page={@pagination} patch={~p"/admin/media"} />
</div>
<%!-- detail panel --%>

View File

@@ -6,8 +6,8 @@ defmodule BerrypodWeb.Admin.Newsletter do
@impl true
def mount(_params, _session, socket) do
counts = Newsletter.count_subscribers_by_status()
subscribers = Newsletter.list_subscribers()
campaigns = Newsletter.list_campaigns()
sub_page = Newsletter.list_subscribers_paginated(page: 1)
camp_page = Newsletter.list_campaigns_paginated(page: 1)
{:ok,
socket
@@ -15,38 +15,47 @@ defmodule BerrypodWeb.Admin.Newsletter do
|> assign(:tab, "overview")
|> assign(:newsletter_enabled, Newsletter.newsletter_enabled?())
|> assign(:status_counts, counts)
|> assign(:subscriber_count, length(subscribers))
|> assign(:campaign_count, length(campaigns))
|> assign(:subscriber_pagination, sub_page)
|> assign(:subscriber_count, sub_page.total_count)
|> assign(:campaign_pagination, camp_page)
|> assign(:campaign_count, camp_page.total_count)
|> assign(:status_filter, "all")
|> assign(:search, "")
|> stream(:subscribers, subscribers)
|> stream(:campaigns, campaigns)}
|> stream(:subscribers, sub_page.items)
|> stream(:campaigns, camp_page.items)}
end
@impl true
def handle_params(%{"tab" => tab}, _uri, socket)
when tab in ~w(overview subscribers campaigns) do
def handle_params(params, _uri, socket) do
tab =
if params["tab"] in ~w(overview subscribers campaigns), do: params["tab"], else: "overview"
page_num = Berrypod.Pagination.parse_page(params)
socket = assign(socket, :tab, tab)
socket =
case tab do
"subscribers" ->
subscribers =
Newsletter.list_subscribers(
page =
Newsletter.list_subscribers_paginated(
status: socket.assigns.status_filter,
search: socket.assigns.search
search: socket.assigns.search,
page: page_num
)
socket
|> assign(:subscriber_count, length(subscribers))
|> stream(:subscribers, subscribers, reset: true)
|> assign(:subscriber_pagination, page)
|> assign(:subscriber_count, page.total_count)
|> stream(:subscribers, page.items, reset: true)
"campaigns" ->
campaigns = Newsletter.list_campaigns()
page = Newsletter.list_campaigns_paginated(page: page_num)
socket
|> assign(:campaign_count, length(campaigns))
|> stream(:campaigns, campaigns, reset: true)
|> assign(:campaign_pagination, page)
|> assign(:campaign_count, page.total_count)
|> stream(:campaigns, page.items, reset: true)
_ ->
socket
@@ -55,8 +64,6 @@ defmodule BerrypodWeb.Admin.Newsletter do
{:noreply, socket}
end
def handle_params(_params, _uri, socket), do: {:noreply, socket}
@impl true
def handle_event("toggle_enabled", _params, socket) do
new_value = !socket.assigns.newsletter_enabled
@@ -71,23 +78,17 @@ defmodule BerrypodWeb.Admin.Newsletter do
end
def handle_event("filter_subscribers", %{"status" => status}, socket) do
subscribers = Newsletter.list_subscribers(status: status, search: socket.assigns.search)
{:noreply,
socket
|> assign(:status_filter, status)
|> assign(:subscriber_count, length(subscribers))
|> stream(:subscribers, subscribers, reset: true)}
|> push_patch(to: ~p"/admin/newsletter?tab=subscribers")}
end
def handle_event("search_subscribers", %{"search" => term}, socket) do
subscribers = Newsletter.list_subscribers(status: socket.assigns.status_filter, search: term)
{:noreply,
socket
|> assign(:search, term)
|> assign(:subscriber_count, length(subscribers))
|> stream(:subscribers, subscribers, reset: true)}
|> push_patch(to: ~p"/admin/newsletter?tab=subscribers")}
end
def handle_event("delete_subscriber", %{"id" => id}, socket) do
@@ -152,12 +153,17 @@ defmodule BerrypodWeb.Admin.Newsletter do
status_filter={@status_filter}
status_counts={@status_counts}
subscriber_count={@subscriber_count}
subscriber_pagination={@subscriber_pagination}
search={@search}
/>
</div>
<div :if={@tab == "campaigns"}>
<.campaigns_tab streams={@streams} campaign_count={@campaign_count} />
<.campaigns_tab
streams={@streams}
campaign_count={@campaign_count}
campaign_pagination={@campaign_pagination}
/>
</div>
"""
end
@@ -254,6 +260,7 @@ defmodule BerrypodWeb.Admin.Newsletter do
attr :status_filter, :string, required: true
attr :status_counts, :map, required: true
attr :subscriber_count, :integer, required: true
attr :subscriber_pagination, Berrypod.Pagination, required: true
attr :search, :string, required: true
defp subscribers_tab(assigns) do
@@ -323,6 +330,13 @@ defmodule BerrypodWeb.Admin.Newsletter do
</:action>
</.table>
<.admin_pagination
:if={@subscriber_count > 0}
page={@subscriber_pagination}
patch={~p"/admin/newsletter"}
params={%{"tab" => "subscribers"}}
/>
<div :if={@subscriber_count == 0} class="text-center py-12 text-base-content/60">
<.icon name="hero-envelope" class="size-12 mx-auto mb-4" />
<p class="text-lg font-medium">No subscribers yet</p>
@@ -336,6 +350,7 @@ defmodule BerrypodWeb.Admin.Newsletter do
attr :streams, :any, required: true
attr :campaign_count, :integer, required: true
attr :campaign_pagination, Berrypod.Pagination, required: true
defp campaigns_tab(assigns) do
~H"""
@@ -370,6 +385,13 @@ defmodule BerrypodWeb.Admin.Newsletter do
</:action>
</.table>
<.admin_pagination
:if={@campaign_count > 0}
page={@campaign_pagination}
patch={~p"/admin/newsletter"}
params={%{"tab" => "campaigns"}}
/>
<div :if={@campaign_count == 0} class="text-center py-12 text-base-content/60">
<.icon name="hero-megaphone" class="size-12 mx-auto mb-4" />
<p class="text-lg font-medium">No campaigns yet</p>

View File

@@ -7,32 +7,38 @@ defmodule BerrypodWeb.Admin.Orders do
@impl true
def mount(_params, _session, socket) do
counts = Orders.count_orders_by_status()
orders = Orders.list_orders()
socket =
socket
|> assign(:page_title, "Orders")
|> assign(:status_filter, "all")
|> assign(:status_counts, counts)
|> assign(:order_count, length(orders))
|> stream(:orders, orders)
{:ok, socket}
end
@impl true
def handle_event("filter", %{"status" => status}, socket) do
orders = Orders.list_orders(status: status)
def handle_params(params, _uri, socket) do
page_num = Berrypod.Pagination.parse_page(params)
page = Orders.list_orders_paginated(status: socket.assigns.status_filter, page: page_num)
socket =
socket
|> assign(:status_filter, status)
|> assign(:order_count, length(orders))
|> stream(:orders, orders, reset: true)
|> assign(:pagination, page)
|> assign(:order_count, page.total_count)
|> stream(:orders, page.items, reset: true)
{:noreply, socket}
end
@impl true
def handle_event("filter", %{"status" => status}, socket) do
{:noreply,
socket
|> assign(:status_filter, status)
|> push_patch(to: ~p"/admin/orders")}
end
@impl true
def render(assigns) do
~H"""
@@ -90,6 +96,8 @@ defmodule BerrypodWeb.Admin.Orders do
</:col>
</.table>
<.admin_pagination :if={@order_count > 0} page={@pagination} patch={~p"/admin/orders"} />
<div :if={@order_count == 0} class="text-center py-12 text-base-content/60">
<.icon name="hero-inbox" class="size-12 mx-auto mb-4" />
<p class="text-lg font-medium">No orders yet</p>

View File

@@ -9,55 +9,58 @@ defmodule BerrypodWeb.Admin.Products do
def mount(_params, _session, socket) do
connections = Products.list_provider_connections()
categories = Products.list_all_categories()
products = Products.list_products_admin()
socket =
socket
|> assign(:page_title, "Products")
|> assign(:connections, connections)
|> assign(:categories, categories)
|> assign(:product_count, length(products))
|> assign(:provider_filter, "all")
|> assign(:category_filter, "all")
|> assign(:visibility_filter, "all")
|> assign(:stock_filter, "all")
|> assign(:sort, "newest")
|> stream(:products, products)
{:ok, socket}
end
@impl true
def handle_event("filter", params, socket) do
provider_filter = params["provider"] || socket.assigns.provider_filter
category_filter = params["category"] || socket.assigns.category_filter
visibility_filter = params["visibility"] || socket.assigns.visibility_filter
stock_filter = params["stock"] || socket.assigns.stock_filter
sort = params["sort"] || socket.assigns.sort
def handle_params(params, _uri, socket) do
page_num = Berrypod.Pagination.parse_page(params)
opts =
[]
|> maybe_add_filter(:provider_connection_id, provider_filter)
|> maybe_add_filter(:category, category_filter)
|> maybe_add_visibility(visibility_filter)
|> maybe_add_stock(stock_filter)
|> Keyword.put(:sort, sort)
build_filter_opts(
socket.assigns.provider_filter,
socket.assigns.category_filter,
socket.assigns.visibility_filter,
socket.assigns.stock_filter,
socket.assigns.sort
)
products = Products.list_products_admin(opts)
page = Products.list_products_admin_paginated([page: page_num] ++ opts)
socket =
socket
|> assign(:provider_filter, provider_filter)
|> assign(:category_filter, category_filter)
|> assign(:visibility_filter, visibility_filter)
|> assign(:stock_filter, stock_filter)
|> assign(:sort, sort)
|> assign(:product_count, length(products))
|> stream(:products, products, reset: true)
|> assign(:pagination, page)
|> assign(:product_count, page.total_count)
|> stream(:products, page.items, reset: true)
{:noreply, socket}
end
@impl true
def handle_event("filter", params, socket) do
socket =
socket
|> assign(:provider_filter, params["provider"] || socket.assigns.provider_filter)
|> assign(:category_filter, params["category"] || socket.assigns.category_filter)
|> assign(:visibility_filter, params["visibility"] || socket.assigns.visibility_filter)
|> assign(:stock_filter, params["stock"] || socket.assigns.stock_filter)
|> assign(:sort, params["sort"] || socket.assigns.sort)
{:noreply, push_patch(socket, to: ~p"/admin/products")}
end
@impl true
def handle_event("toggle_visibility", %{"id" => id}, socket) do
product =
@@ -177,6 +180,8 @@ defmodule BerrypodWeb.Admin.Products do
</:col>
</.table>
<.admin_pagination :if={@product_count > 0} page={@pagination} patch={~p"/admin/products"} />
<div :if={@product_count == 0} class="text-center py-12 text-base-content/60">
<.icon name="hero-cube" class="size-12 mx-auto mb-4" />
<p class="text-lg font-medium">No products yet</p>
@@ -272,6 +277,15 @@ defmodule BerrypodWeb.Admin.Products do
# Filter helpers
# ---------------------------------------------------------------------------
defp build_filter_opts(provider, category, visibility, stock, sort) do
[]
|> maybe_add_filter(:provider_connection_id, provider)
|> maybe_add_filter(:category, category)
|> maybe_add_visibility(visibility)
|> maybe_add_stock(stock)
|> Keyword.put(:sort, sort)
end
defp maybe_add_filter(opts, _key, "all"), do: opts
defp maybe_add_filter(opts, key, value), do: Keyword.put(opts, key, value)

View File

@@ -9,11 +9,16 @@ defmodule BerrypodWeb.Admin.Redirects do
def mount(_params, _session, socket) do
if connected?(socket), do: Redirects.subscribe()
redirect_page = Redirects.list_redirects_paginated(page: 1)
broken_page = Redirects.list_broken_urls_paginated(page: 1)
socket =
socket
|> assign(:page_title, "Redirects")
|> assign(:redirects, Redirects.list_redirects())
|> assign(:broken_urls, Redirects.list_broken_urls())
|> assign(:redirect_pagination, redirect_page)
|> assign(:broken_url_pagination, broken_page)
|> stream(:redirects, redirect_page.items)
|> stream(:broken_urls, broken_page.items)
|> assign(
:form,
to_form(%{"from_path" => "", "to_path" => "", "status_code" => "301"}, as: :redirect)
@@ -25,16 +30,50 @@ defmodule BerrypodWeb.Admin.Redirects do
@impl true
def handle_params(params, _uri, socket) do
tab = if params["tab"] in @valid_tabs, do: params["tab"], else: "redirects"
{:noreply, assign(socket, :tab, tab)}
page_num = Berrypod.Pagination.parse_page(params)
socket = assign(socket, :tab, tab)
socket =
case tab do
"redirects" ->
page = Redirects.list_redirects_paginated(page: page_num)
socket
|> assign(:redirect_pagination, page)
|> stream(:redirects, page.items, reset: true)
"broken" ->
page = Redirects.list_broken_urls_paginated(page: page_num)
socket
|> assign(:broken_url_pagination, page)
|> stream(:broken_urls, page.items, reset: true)
_ ->
socket
end
{:noreply, socket}
end
@impl true
def handle_info({:redirects_changed, _action}, socket) do
{:noreply, assign(socket, :redirects, Redirects.list_redirects())}
page = Redirects.list_redirects_paginated(page: socket.assigns.redirect_pagination.page)
{:noreply,
socket
|> assign(:redirect_pagination, page)
|> stream(:redirects, page.items, reset: true)}
end
def handle_info({:broken_urls_changed, _path}, socket) do
{:noreply, assign(socket, :broken_urls, Redirects.list_broken_urls())}
page = Redirects.list_broken_urls_paginated(page: socket.assigns.broken_url_pagination.page)
{:noreply,
socket
|> assign(:broken_url_pagination, page)
|> stream(:broken_urls, page.items, reset: true)}
end
@impl true
@@ -43,10 +82,15 @@ defmodule BerrypodWeb.Admin.Redirects do
end
def handle_event("delete_redirect", %{"id" => id}, socket) do
redirect = Redirects.get_redirect!(id)
{:ok, _} = Redirects.delete_redirect(redirect)
redirect_rec = Redirects.get_redirect!(id)
{:ok, _} = Redirects.delete_redirect(redirect_rec)
{:noreply, assign(socket, :redirects, Redirects.list_redirects())}
page = Redirects.list_redirects_paginated(page: socket.assigns.redirect_pagination.page)
{:noreply,
socket
|> assign(:redirect_pagination, page)
|> stream(:redirects, page.items, reset: true)}
end
def handle_event("create_redirect", %{"redirect" => params}, socket) do
@@ -80,7 +124,12 @@ defmodule BerrypodWeb.Admin.Redirects do
broken_url = Redirects.get_broken_url!(id)
{:ok, _} = Redirects.ignore_broken_url(broken_url)
{:noreply, assign(socket, :broken_urls, Redirects.list_broken_urls())}
page = Redirects.list_broken_urls_paginated(page: socket.assigns.broken_url_pagination.page)
{:noreply,
socket
|> assign(:broken_url_pagination, page)
|> stream(:broken_urls, page.items, reset: true)}
end
def handle_event("redirect_broken_url", %{"path" => path}, socket) do
@@ -103,17 +152,27 @@ defmodule BerrypodWeb.Admin.Redirects do
</.header>
<div class="flex gap-2 mt-6 mb-4 flex-wrap">
<.tab_button tab="redirects" label="Active" count={length(@redirects)} active={@tab} />
<.tab_button tab="broken" label="Broken URLs" count={length(@broken_urls)} active={@tab} />
<.tab_button
tab="redirects"
label="Active"
count={@redirect_pagination.total_count}
active={@tab}
/>
<.tab_button
tab="broken"
label="Broken URLs"
count={@broken_url_pagination.total_count}
active={@tab}
/>
<.tab_button tab="create" label="Create" active={@tab} />
</div>
<%= if @tab == "redirects" do %>
<.redirects_table redirects={@redirects} />
<.redirects_table streams={@streams} pagination={@redirect_pagination} />
<% end %>
<%= if @tab == "broken" do %>
<.broken_urls_table broken_urls={@broken_urls} />
<.broken_urls_table streams={@streams} pagination={@broken_url_pagination} />
<% end %>
<%= if @tab == "create" do %>
@@ -124,23 +183,23 @@ defmodule BerrypodWeb.Admin.Redirects do
defp redirects_table(assigns) do
~H"""
<%= if @redirects == [] do %>
<%= if @pagination.total_count == 0 do %>
<p>No redirects yet.</p>
<% else %>
<table class="admin-table">
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>Source</th>
<th>Hits</th>
<th>Created</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for redirect <- @redirects do %>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>Source</th>
<th>Hits</th>
<th>Created</th>
<th></th>
</tr>
</thead>
<tbody id="redirects-table" phx-update="stream">
<tr :for={{dom_id, redirect} <- @streams.redirects} id={dom_id}>
<td><code>{redirect.from_path}</code></td>
<td><code>{redirect.to_path}</code></td>
<td>
@@ -161,32 +220,38 @@ defmodule BerrypodWeb.Admin.Redirects do
</button>
</td>
</tr>
<% end %>
</tbody>
</table>
</tbody>
</table>
</div>
<.admin_pagination
page={@pagination}
patch={~p"/admin/redirects"}
params={%{"tab" => "redirects"}}
/>
<% end %>
"""
end
defp broken_urls_table(assigns) do
~H"""
<%= if @broken_urls == [] do %>
<%= if @pagination.total_count == 0 do %>
<p>No broken URLs detected.</p>
<% else %>
<table class="admin-table">
<thead>
<tr>
<th>Path</th>
<th>Prior traffic</th>
<th>404s</th>
<th>First seen</th>
<th>Last seen</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for broken_url <- @broken_urls do %>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Path</th>
<th>Prior traffic</th>
<th>404s</th>
<th>First seen</th>
<th>Last seen</th>
<th></th>
</tr>
</thead>
<tbody id="broken-urls-table" phx-update="stream">
<tr :for={{dom_id, broken_url} <- @streams.broken_urls} id={dom_id}>
<td><code>{broken_url.path}</code></td>
<td>{broken_url.prior_analytics_hits}</td>
<td>{broken_url.recent_404_count}</td>
@@ -209,9 +274,15 @@ defmodule BerrypodWeb.Admin.Redirects do
</button>
</td>
</tr>
<% end %>
</tbody>
</table>
</tbody>
</table>
</div>
<.admin_pagination
page={@pagination}
patch={~p"/admin/redirects"}
params={%{"tab" => "broken"}}
/>
<% end %>
"""
end

View File

@@ -1,7 +1,7 @@
defmodule BerrypodWeb.Shop.Collection do
use BerrypodWeb, :live_view
alias Berrypod.{Pages, Products}
alias Berrypod.{Pages, Pagination, Products}
@sort_options [
{"featured", "Featured"},
@@ -28,18 +28,21 @@ defmodule BerrypodWeb.Shop.Collection do
@impl true
def handle_params(%{"slug" => slug} = params, _uri, socket) do
sort = params["sort"] || "featured"
page_num = Pagination.parse_page(params)
case load_collection(slug, sort) do
{:ok, title, category, products} ->
case load_collection(slug, sort, page_num) do
{:ok, title, category, pagination} ->
{:noreply,
socket
|> assign(:page_title, title)
|> assign(:page_description, collection_description(title))
|> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/collections/#{slug}")
|> assign(:collection_title, title)
|> assign(:collection_slug, slug)
|> assign(:current_category, category)
|> assign(:current_sort, sort)
|> assign(:products, products)}
|> assign(:pagination, pagination)
|> assign(:products, pagination.items)}
:not_found ->
{:noreply,
@@ -49,22 +52,30 @@ defmodule BerrypodWeb.Shop.Collection do
end
end
defp load_collection("all", sort) do
{:ok, "All Products", nil, Products.list_visible_products(sort: sort)}
defp load_collection("all", sort, page) do
pagination = Products.list_visible_products_paginated(sort: sort, page: page)
{:ok, "All Products", nil, pagination}
end
defp load_collection("sale", sort) do
{:ok, "Sale", :sale, Products.list_visible_products(on_sale: true, sort: sort)}
defp load_collection("sale", sort, page) do
pagination = Products.list_visible_products_paginated(on_sale: true, sort: sort, page: page)
{:ok, "Sale", :sale, pagination}
end
defp load_collection(slug, sort) do
defp load_collection(slug, sort, page) do
case Enum.find(Products.list_categories(), &(&1.slug == slug)) do
nil ->
:not_found
category ->
products = Products.list_visible_products(category: category.name, sort: sort)
{:ok, category.name, category, products}
pagination =
Products.list_visible_products_paginated(
category: category.name,
sort: sort,
page: page
)
{:ok, category.name, category, pagination}
end
end