wire collection, PDP, cart, and search pages to page renderer

Stage 4 of the page builder: all shop pages now render via
PageRenderer instead of inline templates or PageTemplates.

- Collection: full filter bar moved to renderer (category pills,
  sort dropdown, CollectionFilters hook, empty state)
- PDP: related_products and reviews loaded via block data loaders
  instead of manual queries
- Cart: page definition loaded in mount, subtotal computed in render
- Search: page definition loaded in mount, handle_params unchanged
- Added Phoenix.VerifiedRoutes to PageRenderer for ~p sigil
- Net -55 lines (128 added, 183 removed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-26 19:13:00 +00:00
parent 914e0661a1
commit 16ebc29fa9
7 changed files with 128 additions and 183 deletions

View File

@@ -1,11 +1,12 @@
defmodule BerrypodWeb.Shop.Cart do
use BerrypodWeb, :live_view
alias Berrypod.Cart
alias Berrypod.{Cart, Pages}
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, page_title: "Cart")}
page = Pages.get_page("cart")
{:ok, socket |> assign(:page_title, "Cart") |> assign(:page, page)}
end
@impl true
@@ -13,7 +14,7 @@ defmodule BerrypodWeb.Shop.Cart do
assigns = assign(assigns, :cart_page_subtotal, Cart.calculate_subtotal(assigns.cart_items))
~H"""
<BerrypodWeb.PageTemplates.cart {assigns} />
<BerrypodWeb.PageRenderer.render_page {assigns} />
"""
end
end

View File

@@ -1,7 +1,7 @@
defmodule BerrypodWeb.Shop.Collection do
use BerrypodWeb, :live_view
alias Berrypod.Products
alias Berrypod.{Pages, Products}
@sort_options [
{"featured", "Featured"},
@@ -14,8 +14,11 @@ defmodule BerrypodWeb.Shop.Collection do
@impl true
def mount(_params, _session, socket) do
page = Pages.get_page("collection")
socket =
socket
|> assign(:page, page)
|> assign(:sort_options, @sort_options)
|> assign(:current_sort, "featured")
@@ -81,113 +84,10 @@ defmodule BerrypodWeb.Shop.Collection do
defp collection_description("Sale"), do: "Browse our current sale items."
defp collection_description(title), do: "Browse our #{String.downcase(title)} collection."
defp collection_path(slug, "featured"), do: ~p"/collections/#{slug}"
defp collection_path(slug, sort), do: ~p"/collections/#{slug}?sort=#{sort}"
@impl true
def render(assigns) do
~H"""
<.shop_layout {layout_assigns(assigns)} active_page="collection">
<main id="main-content">
<.collection_header
title={@collection_title}
product_count={length(@products)}
/>
<div class="page-container collection-body">
<.collection_filter_bar
categories={@categories}
current_slug={
case @current_category do
:sale -> "sale"
nil -> nil
cat -> cat.slug
end
}
sort_options={@sort_options}
current_sort={@current_sort}
/>
<.product_grid theme_settings={@theme_settings}>
<%= for product <- @products do %>
<.product_card
product={product}
theme_settings={@theme_settings}
mode={@mode}
variant={:default}
show_category={@current_category in [nil, :sale]}
/>
<% end %>
</.product_grid>
<%= if @products == [] do %>
<div class="collection-empty">
<p>No products found in this collection.</p>
<.link navigate={~p"/collections/all"} class="collection-empty-link">
View all products
</.link>
</div>
<% end %>
</div>
</main>
</.shop_layout>
"""
end
defp collection_filter_bar(assigns) do
~H"""
<div class="filter-bar">
<nav
aria-label="Collection filters"
id="collection-filters"
phx-hook="CollectionFilters"
class="collection-filters"
>
<ul class="collection-filter-pills">
<li>
<.link
navigate={collection_path("all", @current_sort)}
aria-current={@current_slug == nil && "page"}
class={["collection-filter-pill", @current_slug == nil && "active"]}
>
All
</.link>
</li>
<li>
<.link
navigate={collection_path("sale", @current_sort)}
aria-current={@current_slug == "sale" && "page"}
class={["collection-filter-pill", @current_slug == "sale" && "active"]}
>
Sale
</.link>
</li>
<%= for category <- @categories do %>
<li>
<.link
navigate={collection_path(category.slug, @current_sort)}
aria-current={@current_slug == category.slug && "page"}
class={["collection-filter-pill", @current_slug == category.slug && "active"]}
>
{category.name}
</.link>
</li>
<% end %>
</ul>
</nav>
<form action={~p"/collections/#{@current_slug || "all"}"} method="get" phx-change="sort_changed">
<.shop_select
name="sort"
options={@sort_options}
selected={@current_sort}
aria-label="Sort products"
/>
<noscript>
<button type="submit" class="themed-button collection-sort-submit">Sort</button>
</noscript>
</form>
</div>
<BerrypodWeb.PageRenderer.render_page {assigns} />
"""
end
end

View File

@@ -1,7 +1,7 @@
defmodule BerrypodWeb.Shop.ProductShow do
use BerrypodWeb, :live_view
alias Berrypod.{Analytics, Cart}
alias Berrypod.{Analytics, Cart, Pages}
alias Berrypod.Images.Optimizer
alias Berrypod.Products
alias Berrypod.Products.{Product, ProductImage}
@@ -13,13 +13,6 @@ defmodule BerrypodWeb.Shop.ProductShow do
{:ok, push_navigate(socket, to: ~p"/collections/all")}
product ->
related_products =
Products.list_visible_products(
category: product.category,
limit: 4,
exclude: product.id
)
all_images =
(product.images || [])
|> Enum.sort_by(& &1.position)
@@ -48,6 +41,8 @@ defmodule BerrypodWeb.Shop.ProductShow do
og_url = base <> "/products/#{slug}"
og_image = og_image_url(all_images)
page = Pages.get_page("pdp")
socket =
socket
|> assign(:page_title, product.title)
@@ -58,12 +53,15 @@ defmodule BerrypodWeb.Shop.ProductShow do
|> assign(:json_ld, product_json_ld(product, og_url, og_image, base))
|> assign(:product, product)
|> assign(:all_images, all_images)
|> assign(:related_products, related_products)
|> assign(:quantity, 1)
|> assign(:option_types, option_types)
|> assign(:variants, variants)
|> assign(:page, page)
{:ok, socket}
# Block data loaders (related_products, reviews) run after product is assigned
extra = Pages.load_block_data(page.blocks, socket.assigns)
{:ok, assign(socket, extra)}
end
end
@@ -269,7 +267,7 @@ defmodule BerrypodWeb.Shop.ProductShow do
@impl true
def render(assigns) do
~H"""
<BerrypodWeb.PageTemplates.pdp {assigns} />
<BerrypodWeb.PageRenderer.render_page {assigns} />
"""
end

View File

@@ -1,11 +1,12 @@
defmodule BerrypodWeb.Shop.Search do
use BerrypodWeb, :live_view
alias Berrypod.Search
alias Berrypod.{Pages, Search}
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :page_title, "Search")}
page = Pages.get_page("search")
{:ok, socket |> assign(:page_title, "Search") |> assign(:page, page)}
end
@impl true
@@ -27,49 +28,7 @@ defmodule BerrypodWeb.Shop.Search do
@impl true
def render(assigns) do
~H"""
<.shop_layout {layout_assigns(assigns)} active_page="search">
<main id="main-content" class="page-container">
<.page_title text="Search" />
<form action="/search" method="get" phx-submit="search_submit" class="search-page-form">
<input
type="search"
name="q"
value={@search_page_query}
placeholder="Search products..."
class="themed-input"
/>
<button type="submit" class="themed-button">Search</button>
</form>
<%= if @search_page_results != [] do %>
<p class="search-page-count">
{length(@search_page_results)} {if length(@search_page_results) == 1,
do: "result",
else: "results"} for "{@search_page_query}"
</p>
<.product_grid theme_settings={@theme_settings}>
<%= for product <- @search_page_results do %>
<.product_card
product={product}
theme_settings={@theme_settings}
mode={@mode}
variant={:default}
/>
<% end %>
</.product_grid>
<% else %>
<%= if @search_page_query != "" do %>
<div class="collection-empty">
<p>No products found for "{@search_page_query}"</p>
<.link navigate="/collections/all" class="collection-empty-link">
Browse all products
</.link>
</div>
<% end %>
<% end %>
</main>
</.shop_layout>
<BerrypodWeb.PageRenderer.render_page {assigns} />
"""
end
end

View File

@@ -10,6 +10,11 @@ defmodule BerrypodWeb.PageRenderer do
use Phoenix.Component
use BerrypodWeb.ShopComponents
use Phoenix.VerifiedRoutes,
endpoint: BerrypodWeb.Endpoint,
router: BerrypodWeb.Router,
statics: BerrypodWeb.static_paths()
alias Berrypod.Cart
# ── Public API ──────────────────────────────────────────────────
@@ -263,14 +268,85 @@ defmodule BerrypodWeb.PageRenderer do
end
defp render_block(%{block: %{"type" => "filter_bar"}} = assigns) do
current_slug =
case assigns[:current_category] do
:sale -> "sale"
nil -> nil
cat -> cat.slug
end
assigns =
assigns
|> assign(:current_slug, current_slug)
|> assign(:current_sort, assigns[:current_sort] || "featured")
|> assign(:sort_options, assigns[:sort_options] || [])
~H"""
<div class="page-container">
<.filter_bar categories={assigns[:categories] || []} />
<div class="filter-bar">
<nav
aria-label="Collection filters"
id="collection-filters"
phx-hook="CollectionFilters"
class="collection-filters"
>
<ul class="collection-filter-pills">
<li>
<.link
navigate={collection_path("all", @current_sort)}
aria-current={@current_slug == nil && "page"}
class={["collection-filter-pill", @current_slug == nil && "active"]}
>
All
</.link>
</li>
<li>
<.link
navigate={collection_path("sale", @current_sort)}
aria-current={@current_slug == "sale" && "page"}
class={["collection-filter-pill", @current_slug == "sale" && "active"]}
>
Sale
</.link>
</li>
<%= for category <- assigns[:categories] || [] do %>
<li>
<.link
navigate={collection_path(category.slug, @current_sort)}
aria-current={@current_slug == category.slug && "page"}
class={["collection-filter-pill", @current_slug == category.slug && "active"]}
>
{category.name}
</.link>
</li>
<% end %>
</ul>
</nav>
<form
action={~p"/collections/#{@current_slug || "all"}"}
method="get"
phx-change="sort_changed"
>
<.shop_select
name="sort"
options={@sort_options}
selected={@current_sort}
aria-label="Sort products"
/>
<noscript>
<button type="submit" class="themed-button collection-sort-submit">Sort</button>
</noscript>
</form>
</div>
</div>
"""
end
defp render_block(%{block: %{"type" => "product_grid"}} = assigns) do
show_category = assigns[:current_category] in [nil, :sale]
assigns = assign(assigns, :show_category, show_category)
~H"""
<div class="page-container">
<.product_grid theme_settings={@theme_settings}>
@@ -280,10 +356,19 @@ defmodule BerrypodWeb.PageRenderer do
theme_settings={@theme_settings}
mode={@mode}
variant={:default}
show_category={true}
show_category={@show_category}
/>
<% end %>
</.product_grid>
<%= if (assigns[:products] || []) == [] do %>
<div class="collection-empty">
<p>No products found in this collection.</p>
<.link navigate={~p"/collections/all"} class="collection-empty-link">
View all products
</.link>
</div>
<% end %>
</div>
"""
end
@@ -722,6 +807,7 @@ defmodule BerrypodWeb.PageRenderer do
def page_main_class("orders"), do: "page-container orders-main"
def page_main_class("order_detail"), do: "page-container order-detail-main"
def page_main_class("error"), do: "error-main"
def page_main_class("collection"), do: nil
def page_main_class("pdp"), do: "page-container"
def page_main_class("search"), do: "page-container"
def page_main_class("about"), do: "content-page"
@@ -767,6 +853,9 @@ defmodule BerrypodWeb.PageRenderer do
defp breadcrumb_items(_), do: []
defp collection_path(slug, "featured"), do: ~p"/collections/#{slug}"
defp collection_path(slug, sort), do: ~p"/collections/#{slug}?sort=#{sort}"
# Reuse from PageTemplates
def format_order_status("unfulfilled"), do: "Being prepared"
def format_order_status("submitted"), do: "Sent to printer"