integrate R module and add url editor ui

Replaces hardcoded paths with R module throughout:
- Shop components: layout nav, cart, product links
- Controllers: cart, checkout, contact, seo, order lookup
- Shop pages: collection, product, search, checkout success, etc.
- Site context: nav item url resolution

Admin URL management:
- Settings page: prefix editor with validation feedback
- Page renderer: url_editor component for page URLs
- CSS for url editor styling

Test updates for cache isolation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-04-01 00:36:17 +01:00
parent c115f08cb8
commit a41771efc8
28 changed files with 938 additions and 160 deletions

View File

@@ -19,6 +19,7 @@ defmodule BerrypodWeb.PageRenderer do
import BerrypodWeb.CoreComponents, only: [icon: 1, external_link: 1]
alias Berrypod.Cart
alias BerrypodWeb.R
# ── Public API ──────────────────────────────────────────────────
@@ -88,6 +89,7 @@ defmodule BerrypodWeb.PageRenderer do
editor_dirty={@editor_dirty}
theme_dirty={Map.get(assigns, :theme_dirty, false)}
site_dirty={Map.get(assigns, :site_dirty, false)}
settings_dirty={Map.get(assigns, :settings_dirty, false)}
editor_sheet_state={assigns[:editor_sheet_state] || :collapsed}
editor_save_status={@editor_save_status}
editor_active_tab={Map.get(assigns, :editor_active_tab, :page)}
@@ -373,14 +375,18 @@ defmodule BerrypodWeb.PageRenderer do
defp page_settings_section(assigns) do
form = assigns.form || %{}
is_custom = assigns.page[:type] == "custom"
page = assigns.page
# Determine URL structure for this page
url_info = compute_url_info(page, form)
assigns =
assigns
|> assign(:is_custom, is_custom)
|> assign(:url_info, url_info)
|> assign(:form_title, form["title"] || page[:title] || "")
|> assign(:form_slug, form["slug"] || page[:slug] || "")
|> assign(:form_url_slug, form["url_slug"] || page[:url_slug] || "")
|> assign(:form_meta, form["meta_description"] || page[:meta_description] || "")
|> assign(:form_published, form_checked?(form, "published", page[:published]))
|> assign(:form_show_in_nav, form_checked?(form, "show_in_nav", page[:show_in_nav]))
@@ -415,22 +421,8 @@ defmodule BerrypodWeb.PageRenderer do
/>
</div>
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-slug">URL slug</label>
<div class="page-settings-slug-input">
<span class="page-settings-slug-prefix">/</span>
<input
type="text"
id="page-settings-slug"
name="page[slug]"
value={@form_slug}
class={["admin-input", !@is_custom && "admin-input-disabled"]}
pattern="[a-z0-9-]+"
disabled={!@is_custom}
title={if !@is_custom, do: "System page URLs cannot be changed", else: nil}
/>
</div>
</div>
<%!-- URL editor - different layouts based on page type --%>
<.url_editor url_info={@url_info} is_custom={@is_custom} form_slug={@form_slug} />
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-meta">Meta description</label>
@@ -497,6 +489,166 @@ defmodule BerrypodWeb.PageRenderer do
"""
end
# URL editor component - renders the right UI based on page type
attr :url_info, :map, required: true
attr :is_custom, :boolean, required: true
attr :form_slug, :string, required: true
defp url_editor(%{url_info: %{type: :none}} = assigns) do
# Home page - no URL field
~H""
end
defp url_editor(%{url_info: %{type: :simple}} = assigns) do
# Simple single-segment URL (about, contact, cart, custom pages, etc.)
~H"""
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-url">URL</label>
<div class="url-editor">
<span class="url-editor-slash">/</span>
<input
type="text"
id="page-settings-url"
name={@url_info.field_name}
value={@url_info.value}
class="admin-input url-editor-input"
pattern="[a-z0-9-]+"
phx-debounce="500"
/>
</div>
<p :if={@url_info.hint} class="page-settings-hint">{@url_info.hint}</p>
</div>
"""
end
defp url_editor(%{url_info: %{type: :prefixed}} = assigns) do
# Prefixed URL (collections, products, orders)
~H"""
<div class="page-settings-field">
<label class="page-settings-label">URL</label>
<div class="url-editor url-editor-segmented">
<span class="url-editor-slash">/</span>
<input
type="text"
id="page-settings-prefix"
name="page[url_prefix]"
value={@url_info.prefix}
class="admin-input url-editor-input url-editor-prefix"
pattern="[a-z0-9-]+"
phx-debounce="500"
/>
<span class="url-editor-slash">/</span>
<span class="url-editor-fixed">{@url_info.suffix}</span>
</div>
<p :if={@url_info.hint} class="page-settings-hint">{@url_info.hint}</p>
</div>
"""
end
defp url_editor(%{url_info: %{type: :fixed}} = assigns) do
# Fixed URL (checkout/success, etc.)
~H"""
<div class="page-settings-field">
<label class="page-settings-label">URL</label>
<div class="url-editor">
<span class="url-editor-fixed-path">{@url_info.path}</span>
</div>
</div>
"""
end
# Compute URL info for a page based on its type
# Uses generic placeholders for prefixed pages since we're editing the template
defp compute_url_info(page, form) do
slug = page[:slug]
case slug do
# Home - no URL field
"home" ->
%{type: :none}
# Prefixed pages - show editable prefix + placeholder suffix
"collection" ->
prefix = form["url_prefix"] || R.prefix(:collections)
%{
type: :prefixed,
prefix: prefix,
prefix_type: :collections,
suffix: ":slug",
hint: "Changing the prefix affects all collection pages"
}
"pdp" ->
prefix = form["url_prefix"] || R.prefix(:products)
%{
type: :prefixed,
prefix: prefix,
prefix_type: :products,
suffix: ":id",
hint: "Changing the prefix affects all product pages"
}
"orders" ->
prefix = form["url_prefix"] || R.prefix(:orders)
%{
type: :prefixed,
prefix: prefix,
prefix_type: :orders,
suffix: ":number",
hint: "Changing the prefix affects all order pages"
}
"order_detail" ->
prefix = form["url_prefix"] || R.prefix(:orders)
%{
type: :prefixed,
prefix: prefix,
prefix_type: :orders,
suffix: ":number",
hint: "Changing the prefix affects all order pages"
}
# Fixed paths
"checkout_success" ->
%{type: :fixed, path: "/checkout/success"}
"error" ->
%{type: :none}
# Simple system pages and custom pages
_ ->
if page[:type] == "custom" do
# Custom page - edit the slug directly
value = form["slug"] || slug
%{
type: :simple,
value: value,
field_name: "page[slug]",
hint: nil
}
else
# System page - edit the url_slug override
value = form["url_slug"] || page[:url_slug] || ""
%{
type: :simple,
value: value,
field_name: "page[url_slug]",
hint:
if(value == "",
do: "Default: /#{slug}",
else: "Old URL /#{slug} will redirect here"
)
}
end
end
end
defp form_checked?(form, key, page_value) when is_map(form) do
case form[key] do
"true" -> true
@@ -842,7 +994,7 @@ defmodule BerrypodWeb.PageRenderer do
</nav>
<form
action={~p"/collections/#{@current_slug || "all"}"}
action={R.collection(@current_slug || "all")}
method="get"
phx-change="sort_changed"
>
@@ -890,14 +1042,14 @@ defmodule BerrypodWeb.PageRenderer do
<.shop_pagination
:if={assigns[:pagination] && assigns[:pagination].total_pages > 1}
page={assigns[:pagination]}
base_path={~p"/collections/#{@collection_slug}"}
base_path={R.collection(@collection_slug)}
params={@sort_params}
/>
<%= if (assigns[:products] || []) == [] do %>
<div class="collection-empty">
<p>No products found in this collection.</p>
<.link patch={~p"/collections/all"} class="collection-empty-link">
<.link patch={R.collection("all")} class="collection-empty-link">
View all products
</.link>
</div>
@@ -1075,7 +1227,7 @@ defmodule BerrypodWeb.PageRenderer do
<% end %>
<div class="checkout-actions">
<.shop_link_button href="/collections/all" class="checkout-cta">
<.shop_link_button href={R.collection("all")} class="checkout-cta">
Continue shopping
</.shop_link_button>
</div>
@@ -1104,7 +1256,7 @@ defmodule BerrypodWeb.PageRenderer do
Please wait while we confirm your payment. This usually takes a few seconds.
</p>
<p class="checkout-pending-hint">
If this page doesn't update, please <.link patch="/contact" class="checkout-contact-link">contact us</.link>.
If this page doesn't update, please <.link patch={R.contact()} class="checkout-contact-link">contact us</.link>.
</p>
</div>
<% end %>
@@ -1129,20 +1281,20 @@ defmodule BerrypodWeb.PageRenderer do
<div class="orders-empty">
<p>This link has expired or is invalid.</p>
<p class="orders-empty-hint">
Head back to the <.link patch="/contact">contact page</.link> to request a new one.
Head back to the <.link patch={R.contact()}>contact page</.link> to request a new one.
</p>
</div>
<% assigns[:orders] == [] -> %>
<div class="orders-empty">
<p>No orders found for that email address.</p>
<p class="orders-empty-hint">
If something doesn't look right, <.link patch="/contact">get in touch</.link>.
If something doesn't look right, <.link patch={R.contact()}>get in touch</.link>.
</p>
</div>
<% true -> %>
<div class="orders-list">
<%= for order <- assigns[:orders] do %>
<.link patch={"/orders/#{order.order_number}"} class="order-summary-card">
<.link patch={R.order(order.order_number)} class="order-summary-card">
<div class="order-summary-top">
<div>
<p class="order-summary-number">{order.order_number}</p>
@@ -1184,7 +1336,7 @@ defmodule BerrypodWeb.PageRenderer do
~H"""
<%= if assigns[:order] do %>
<div class="order-detail-header">
<.link patch="/orders" class="order-detail-back">&larr; Back to orders</.link>
<.link patch={R.orders()} class="order-detail-back">&larr; Back to orders</.link>
<h1 class="checkout-heading" style="margin-top: 1.5rem;">{assigns[:order].order_number}</h1>
<p class="checkout-meta">{Calendar.strftime(assigns[:order].inserted_at, "%-d %B %Y")}</p>
<span class={"order-status-badge order-status-badge-#{assigns[:order].fulfilment_status} order-status-badge-lg"}>
@@ -1239,7 +1391,7 @@ defmodule BerrypodWeb.PageRenderer do
<div>
<%= if info && info.slug do %>
<.link
patch={"/products/#{info.slug}"}
patch={R.product(info.slug)}
class="checkout-item-name checkout-item-link"
>
{item.product_name}
@@ -1286,7 +1438,7 @@ defmodule BerrypodWeb.PageRenderer do
<% end %>
<div class="checkout-actions">
<.shop_link_button href="/collections/all">Continue shopping</.shop_link_button>
<.shop_link_button href={R.collection("all")}>Continue shopping</.shop_link_button>
</div>
<% end %>
"""
@@ -1298,7 +1450,7 @@ defmodule BerrypodWeb.PageRenderer do
~H"""
<.page_title text="Search" />
<form action="/search" method="get" phx-submit="search_submit" class="search-page-form">
<form action={R.search()} method="get" phx-submit="search_submit" class="search-page-form">
<input
type="search"
name="q"
@@ -1329,7 +1481,7 @@ defmodule BerrypodWeb.PageRenderer do
<%= if (assigns[:search_page_query] || "") != "" do %>
<div class="collection-empty">
<p>No products found for &ldquo;{assigns[:search_page_query]}&rdquo;</p>
<.link patch="/collections/all" class="collection-empty-link">Browse all products</.link>
<.link patch={R.collection("all")} class="collection-empty-link">Browse all products</.link>
</div>
<% end %>
<% end %>
@@ -1469,7 +1621,7 @@ defmodule BerrypodWeb.PageRenderer do
slug = cat |> String.downcase() |> String.replace(" ", "-")
[
%{label: cat, page: "collection", href: "/collections/#{slug}"},
%{label: cat, page: "collection", href: R.collection(slug)},
%{label: title, current: true}
]
end
@@ -1480,8 +1632,8 @@ 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}"
defp collection_path(slug, "featured"), do: R.collection(slug)
defp collection_path(slug, sort), do: R.collection(slug) <> "?sort=#{sort}"
# Resolves an image_id to a full URL for blocks that need a single URL (e.g. background-image).
defp resolve_block_image_url(image_id) do