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:
@@ -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">← Back to orders</.link>
|
||||
<.link patch={R.orders()} class="order-detail-back">← 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 “{assigns[:search_page_query]}”</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
|
||||
|
||||
Reference in New Issue
Block a user