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:
@@ -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 --%>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user