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

@@ -6,6 +6,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
import BerrypodWeb.ShopComponents.Base
alias Berrypod.Products.{Product, ProductImage}
alias BerrypodWeb.R
defp close_cart_drawer_js do
Phoenix.LiveView.JS.push("close_cart_drawer")
@@ -193,7 +194,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
>
<%= if @mode != :preview do %>
<.link
patch={"/products/#{@item.product_id}"}
patch={R.product(@item.product_id)}
class={["cart-item-image", !@item.image && "cart-item-image--empty"]}
data-size={if @size == :compact, do: "compact"}
style={if @item.image, do: "background-image: url('#{@item.image}');"}
@@ -212,7 +213,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
<h3 class="cart-item-name" data-size={if @size == :compact, do: "compact"}>
<%= if @mode != :preview do %>
<.link
patch={"/products/#{@item.product_id}"}
patch={R.product(@item.product_id)}
class="cart-item-name-link"
>
{@item.name}
@@ -321,7 +322,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
</button>
<% else %>
<.link
patch="/collections/all"
patch={R.collection("all")}
class="cart-continue-link"
>
Continue shopping
@@ -533,7 +534,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
</.shop_button>
<p class="order-summary-notice">Checkout isn't available yet.</p>
<.shop_link_outline
href="/collections/all"
href={R.collection("all")}
class="order-summary-continue"
>
Continue shopping
@@ -544,7 +545,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
</.shop_button>
<p class="order-summary-notice">Remove unavailable items to checkout.</p>
<.shop_link_outline
href="/collections/all"
href={R.collection("all")}
class="order-summary-continue"
>
Continue shopping
@@ -557,7 +558,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
</.shop_button>
</form>
<.shop_link_outline
href="/collections/all"
href={R.collection("all")}
class="order-summary-continue"
>
Continue shopping

View File

@@ -4,6 +4,8 @@ defmodule BerrypodWeb.ShopComponents.Layout do
import BerrypodWeb.ShopComponents.Cart
import BerrypodWeb.ShopComponents.Content
alias BerrypodWeb.R
@doc """
Renders the announcement bar.
@@ -153,10 +155,18 @@ defmodule BerrypodWeb.ShopComponents.Layout do
# Extract a slug from a URL for nav item matching
defp extract_slug_from_url(url) when is_binary(url) do
cond do
url == "/" -> "home"
String.starts_with?(url, "/collections") -> "collection"
String.starts_with?(url, "/products") -> "pdp"
true -> url |> String.trim_leading("/") |> String.split("/") |> List.first() || ""
url == "/" ->
"home"
true ->
# Extract first segment and check if it's a known prefix
first_segment = url |> String.trim_leading("/") |> String.split("/") |> List.first() || ""
case R.prefix_type_from_segment(first_segment) do
:collections -> "collection"
:products -> "pdp"
_ -> first_segment
end
end
end
@@ -651,7 +661,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
aria-selected="false"
>
<.link
patch={"/products/#{item.product.slug || item.product.id}"}
patch={R.product(item.product.slug || item.product.id)}
class="search-result"
phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")}
>
@@ -782,7 +792,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
</a>
<% else %>
<.link
patch={"/collections/#{category.slug}"}
patch={R.collection(category.slug)}
class="mobile-nav-link"
phx-click={
Phoenix.LiveView.JS.dispatch("close-mobile-nav", to: "#mobile-nav-drawer")
@@ -868,7 +878,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<% else %>
<li>
<.link
patch="/collections/all"
patch={R.collection("all")}
class="footer-link"
>
All products
@@ -877,7 +887,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<%= for category <- @categories do %>
<li>
<.link
patch={"/collections/#{category.slug}"}
patch={R.collection(category.slug)}
class="footer-link"
>
{category.name}
@@ -1013,7 +1023,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<.admin_cog_svg />
</.link>
<a
href="/search"
href={R.search()}
phx-click={Phoenix.LiveView.JS.dispatch("open-search", to: "#search-modal")}
class="header-icon-btn"
aria-label="Search"
@@ -1031,7 +1041,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
</svg>
</a>
<a
href="/cart"
href={R.cart()}
phx-click={open_cart_drawer_js()}
class="header-icon-btn"
aria-label="Cart"
@@ -1241,6 +1251,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
attr :editor_dirty, :boolean, default: false
attr :theme_dirty, :boolean, default: false
attr :site_dirty, :boolean, default: false
attr :settings_dirty, :boolean, default: false
attr :editor_sheet_state, :atom, default: :collapsed
attr :editor_save_status, :atom, default: :idle
attr :editor_active_tab, :atom, default: :page
@@ -1263,7 +1274,8 @@ defmodule BerrypodWeb.ShopComponents.Layout do
any_editing = assigns.editing || assigns.theme_editing
# Any tab has unsaved changes
any_dirty = assigns.editor_dirty || assigns.theme_dirty || assigns.site_dirty
any_dirty =
assigns.editor_dirty || assigns.theme_dirty || assigns.site_dirty || assigns.settings_dirty
assigns =
assigns

View File

@@ -5,6 +5,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
import BerrypodWeb.ShopComponents.Content, only: [responsive_image: 1]
alias Berrypod.Products.{Product, ProductImage}
alias BerrypodWeb.R
@doc """
Renders a product card with configurable variants.
@@ -98,7 +99,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
product_url =
if assigns.clickable && assigns.mode != :preview do
"/products/#{Map.get(assigns.product, :slug) || Map.get(assigns.product, :id)}"
R.product(Map.get(assigns.product, :slug) || Map.get(assigns.product, :id))
end
assigns =
@@ -157,7 +158,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
</p>
<% else %>
<.link
patch={"/collections/#{Slug.slugify(@product.category)}"}
patch={R.collection(Slug.slugify(@product.category))}
class="product-card-category"
>
{@product.category}
@@ -177,7 +178,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
</a>
<% else %>
<.link
patch={"/products/#{Map.get(@product, :slug) || Map.get(@product, :id)}"}
patch={R.product(Map.get(@product, :slug) || Map.get(@product, :id))}
class="stretched-link"
>
{@product.title}
@@ -629,7 +630,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
</a>
<% else %>
<.link
patch={"/collections/#{category.slug}"}
patch={R.collection(category.slug)}
class="category-card"
>
<div

View File

@@ -41,13 +41,13 @@ defmodule BerrypodWeb.CartController do
conn
|> Cart.put_in_session(cart)
|> put_flash(:info, "Added to basket")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
end
def add(conn, _params) do
conn
|> put_flash(:error, "Could not add item to basket")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
end
@doc """
@@ -60,7 +60,7 @@ defmodule BerrypodWeb.CartController do
conn
|> Cart.put_in_session(cart)
|> put_flash(:info, "Removed from basket")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
end
@doc """
@@ -73,7 +73,7 @@ defmodule BerrypodWeb.CartController do
conn
|> Cart.put_in_session(cart)
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
end
@doc """
@@ -82,11 +82,11 @@ defmodule BerrypodWeb.CartController do
def update_country(conn, %{"country" => code}) when is_binary(code) and code != "" do
conn
|> put_session("country_code", code)
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
end
def update_country(conn, _params) do
redirect(conn, to: ~p"/cart")
redirect(conn, to: R.cart())
end
defp parse_quantity(str) when is_binary(str) do

View File

@@ -11,7 +11,7 @@ defmodule BerrypodWeb.CheckoutController do
unless Settings.has_secret?("stripe_api_key") do
conn
|> put_flash(:error, "Checkout isn't available yet")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
else
cart_items = Cart.get_from_session(get_session(conn))
hydrated = Cart.hydrate(cart_items)
@@ -20,12 +20,12 @@ defmodule BerrypodWeb.CheckoutController do
hydrated == [] ->
conn
|> put_flash(:error, "Your basket is empty")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
Enum.any?(hydrated, &(&1.is_available == false)) ->
conn
|> put_flash(:error, "Some items in your basket are no longer available")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
true ->
track_checkout_start(conn)
@@ -45,7 +45,7 @@ defmodule BerrypodWeb.CheckoutController do
conn
|> put_flash(:error, "Something went wrong. Please try again.")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
end
end
@@ -67,14 +67,12 @@ defmodule BerrypodWeb.CheckoutController do
}
end)
base_url = BerrypodWeb.Endpoint.url()
params =
%{
mode: "payment",
line_items: line_items,
success_url: "#{base_url}/checkout/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "#{base_url}/cart",
success_url: R.url(R.checkout_success()) <> "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: R.url(R.cart()),
metadata: %{"order_id" => order.id},
shipping_address_collection: %{
allowed_countries: ["GB", "US", "CA", "AU", "DE", "FR", "NL", "IE", "AT", "BE"]
@@ -96,7 +94,7 @@ defmodule BerrypodWeb.CheckoutController do
conn
|> put_flash(:error, "Payment setup failed. Please try again.")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
{:error, reason} ->
Logger.error("Stripe session creation failed: #{inspect(reason)}")
@@ -104,7 +102,7 @@ defmodule BerrypodWeb.CheckoutController do
conn
|> put_flash(:error, "Payment setup failed. Please try again.")
|> redirect(to: ~p"/cart")
|> redirect(to: R.cart())
end
end

View File

@@ -13,17 +13,17 @@ defmodule BerrypodWeb.ContactController do
{:ok, _} ->
conn
|> put_flash(:info, "Message sent! We'll get back to you soon.")
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
{:error, :invalid_params} ->
conn
|> put_flash(:error, "Please fill in all required fields.")
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
{:error, _} ->
conn
|> put_flash(:error, "Sorry, something went wrong. Please try again.")
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
end
end
end

View File

@@ -19,7 +19,7 @@ defmodule BerrypodWeb.OrderLookupController do
:error,
"No orders found for that address. Make sure you use the same email you checked out with."
)
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
else
token = generate_token(email)
link = BerrypodWeb.Endpoint.url() <> ~p"/orders/verify/#{token}"
@@ -30,14 +30,14 @@ defmodule BerrypodWeb.OrderLookupController do
:info,
"We've sent a link to your email address. It'll expire after an hour."
)
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
end
end
def lookup(conn, _params) do
conn
|> put_flash(:error, "Please enter your email address.")
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
end
def verify(conn, %{"token" => token}) do
@@ -45,17 +45,17 @@ defmodule BerrypodWeb.OrderLookupController do
{:ok, email} ->
conn
|> put_session(:order_lookup_email, email)
|> redirect(to: ~p"/orders")
|> redirect(to: R.orders())
{:error, :expired} ->
conn
|> put_flash(:error, "That link has expired. Please request a new one.")
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
{:error, _} ->
conn
|> put_flash(:error, "That link is invalid.")
|> redirect(to: ~p"/contact")
|> redirect(to: R.contact())
end
end

View File

@@ -2,6 +2,7 @@ defmodule BerrypodWeb.SeoController do
use BerrypodWeb, :controller
alias Berrypod.{Pages, Products}
alias BerrypodWeb.R
def robots(conn, _params) do
base = BerrypodWeb.Endpoint.url()
@@ -29,23 +30,23 @@ defmodule BerrypodWeb.SeoController do
categories = Products.list_categories()
static_pages = [
{"/", "daily", "1.0"},
{"/collections/all", "daily", "0.9"},
{"/about", "monthly", "0.5"},
{"/contact", "monthly", "0.5"},
{"/delivery", "monthly", "0.5"},
{"/privacy", "monthly", "0.3"},
{"/terms", "monthly", "0.3"}
{R.home(), "daily", "1.0"},
{R.collection("all"), "daily", "0.9"},
{R.about(), "monthly", "0.5"},
{R.contact(), "monthly", "0.5"},
{R.delivery(), "monthly", "0.5"},
{R.privacy(), "monthly", "0.3"},
{R.terms(), "monthly", "0.3"}
]
category_pages =
Enum.map(categories, fn cat ->
{"/collections/#{cat.slug}", "daily", "0.8"}
{R.collection(cat.slug), "daily", "0.8"}
end)
product_pages =
Enum.map(products, fn product ->
{"/products/#{product.slug}", "weekly", "0.9"}
{R.product(product.slug), "weekly", "0.9"}
end)
custom_pages =

View File

@@ -4,6 +4,7 @@ defmodule BerrypodWeb.Admin.ProductShow do
alias Berrypod.Products
alias Berrypod.Products.{Product, ProductImage, ProductVariant}
alias Berrypod.Cart
alias BerrypodWeb.R
@impl true
def mount(%{"id" => id}, _session, socket) do
@@ -109,7 +110,7 @@ defmodule BerrypodWeb.Admin.ProductShow do
<.icon name="hero-arrow-top-right-on-square-mini" class="size-4" />
</.external_link>
<.link
navigate={~p"/products/#{@product.slug}"}
navigate={R.product(@product.slug)}
class="admin-btn admin-btn-ghost admin-btn-sm"
>
View on shop <.icon name="hero-arrow-top-right-on-square-mini" class="size-4" />

View File

@@ -18,7 +18,15 @@ defmodule BerrypodWeb.Admin.Settings do
|> assign(:from_address_status, :idle)
|> assign(:signing_secret_status, :idle)
|> assign_stripe_state()
|> assign_products_state()}
|> assign_products_state()
|> assign_url_prefixes()}
end
defp assign_url_prefixes(socket) do
socket
|> assign(:products_prefix, Settings.get_url_prefix(:products))
|> assign(:collections_prefix, Settings.get_url_prefix(:collections))
|> assign(:prefix_status, :idle)
end
# -- Stripe assigns --
@@ -117,6 +125,51 @@ defmodule BerrypodWeb.Admin.Settings do
end
end
# -- Events: URL prefixes --
def handle_event("change_prefix", _params, socket) do
{:noreply, assign(socket, :prefix_status, :idle)}
end
def handle_event("save_prefixes", %{"prefixes" => params}, socket) do
products_prefix = params["products"] || ""
collections_prefix = params["collections"] || ""
errors = []
# Update products prefix if changed
errors =
if products_prefix != socket.assigns.products_prefix do
case Settings.update_url_prefix(:products, products_prefix) do
{:ok, _} -> errors
{:error, reason} -> [{:products, reason} | errors]
end
else
errors
end
# Update collections prefix if changed
errors =
if collections_prefix != socket.assigns.collections_prefix do
case Settings.update_url_prefix(:collections, collections_prefix) do
{:ok, _} -> errors
{:error, reason} -> [{:collections, reason} | errors]
end
else
errors
end
if errors == [] do
{:noreply,
socket
|> assign_url_prefixes()
|> assign(:prefix_status, :saved)}
else
error_message = format_prefix_errors(errors)
{:noreply, put_flash(socket, :error, error_message)}
end
end
# -- Events: Stripe --
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
@@ -388,6 +441,67 @@ defmodule BerrypodWeb.Admin.Settings do
</form>
</div>
</section>
<%!-- URL prefixes --%>
<section class="admin-section">
<h2 class="admin-section-title">URL prefixes</h2>
<p class="admin-section-desc">
Customise the URL structure for products and collections.
Old URLs will automatically redirect to the new ones.
</p>
<div class="admin-section-body">
<form
phx-change="change_prefix"
phx-submit="save_prefixes"
class="admin-stack"
>
<div class="admin-row admin-row-lg">
<label class="admin-label admin-label-inline" for="prefix-products">
Products
</label>
<div class="admin-row admin-row-sm">
<span class="admin-text-secondary">/</span>
<.input
name="prefixes[products]"
id="prefix-products"
value={@products_prefix}
placeholder="products"
pattern="[a-z0-9-]+"
/>
<span class="admin-text-secondary">/</span>
<span class="admin-text-tertiary">product-slug</span>
</div>
</div>
<div class="admin-row admin-row-lg">
<label class="admin-label admin-label-inline" for="prefix-collections">
Collections
</label>
<div class="admin-row admin-row-sm">
<span class="admin-text-secondary">/</span>
<.input
name="prefixes[collections]"
id="prefix-collections"
value={@collections_prefix}
placeholder="collections"
pattern="[a-z0-9-]+"
/>
<span class="admin-text-secondary">/</span>
<span class="admin-text-tertiary">collection-slug</span>
</div>
</div>
<p class="admin-help-text">
Use only lowercase letters, numbers, and hyphens.
</p>
<div class="admin-form-actions-sm">
<.button phx-disable-with="Saving...">Save</.button>
<.inline_feedback status={@prefix_status} />
</div>
</form>
</div>
</section>
</div>
"""
end
@@ -639,4 +753,20 @@ defmodule BerrypodWeb.Admin.Settings do
true -> "#{div(diff, 86400)} days ago"
end
end
defp format_prefix_errors(errors) do
Enum.map_join(errors, "; ", fn {field, reason} ->
field_name = if field == :products, do: "Products", else: "Collections"
message =
case reason do
:empty_prefix -> "can't be blank"
:invalid_format -> "must contain only letters, numbers, and hyphens"
:reserved_prefix -> "is reserved"
_ -> "is invalid"
end
"#{field_name} prefix #{message}"
end)
end
end

View File

@@ -8,6 +8,7 @@ defmodule BerrypodWeb.Shop.Pages.CheckoutSuccess do
import Phoenix.LiveView, only: [connected?: 1, redirect: 2]
alias Berrypod.{Analytics, Orders, Pages}
alias BerrypodWeb.R
def init(socket, %{"session_id" => session_id}, _uri) do
order = Orders.get_order_by_stripe_session(session_id)
@@ -21,7 +22,7 @@ defmodule BerrypodWeb.Shop.Pages.CheckoutSuccess do
if order && connected?(socket) && socket.assigns[:analytics_visitor_hash] do
attrs =
BerrypodWeb.AnalyticsHook.attrs(socket)
|> Map.merge(%{pathname: "/checkout/success", revenue: order.total})
|> Map.merge(%{pathname: R.checkout_success(), revenue: order.total})
Analytics.track_event("purchase", attrs)
end
@@ -54,7 +55,7 @@ defmodule BerrypodWeb.Shop.Pages.CheckoutSuccess do
end
def init(socket, _params, _uri) do
{:redirect, redirect(socket, to: "/")}
{:redirect, redirect(socket, to: R.home())}
end
def handle_params(_params, _uri, socket) do

View File

@@ -7,6 +7,7 @@ defmodule BerrypodWeb.Shop.Pages.Collection do
import Phoenix.LiveView, only: [push_patch: 2, push_navigate: 2, put_flash: 3]
alias Berrypod.{Pages, Pagination, Products}
alias BerrypodWeb.R
@sort_options [
{"featured", "Featured"},
@@ -29,6 +30,11 @@ defmodule BerrypodWeb.Shop.Pages.Collection do
{:noreply, socket}
end
# When accessed via custom URL (e.g. /shop) without a collection slug, show all products
def handle_params(params, uri, socket) when not is_map_key(params, "slug") do
handle_params(Map.put(params, "slug", "all"), uri, socket)
end
def handle_params(%{"slug" => slug} = params, _uri, socket) do
sort = params["sort"] || "featured"
page_num = Pagination.parse_page(params)
@@ -39,7 +45,7 @@ defmodule BerrypodWeb.Shop.Pages.Collection do
socket
|> assign(:page_title, title)
|> assign(:page_description, collection_description(title))
|> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/collections/#{slug}")
|> assign(:og_url, R.url(R.collection(slug)))
|> assign(:collection_title, title)
|> assign(:collection_slug, slug)
|> assign(:current_category, category)
@@ -53,7 +59,7 @@ defmodule BerrypodWeb.Shop.Pages.Collection do
socket =
socket
|> put_flash(:error, "Collection not found")
|> push_navigate(to: "/collections/all")
|> push_navigate(to: R.collection("all"))
{:noreply, socket}
end
@@ -67,7 +73,7 @@ defmodule BerrypodWeb.Shop.Pages.Collection do
category -> category.slug
end
{:noreply, push_patch(socket, to: "/collections/#{slug}?sort=#{sort}")}
{:noreply, push_patch(socket, to: R.collection(slug) <> "?sort=#{sort}")}
end
def handle_event(_event, _params, _socket), do: :cont

View File

@@ -7,6 +7,7 @@ defmodule BerrypodWeb.Shop.Pages.Contact do
import Phoenix.LiveView, only: [push_navigate: 2, put_flash: 3]
alias Berrypod.{ContactNotifier, Orders}
alias BerrypodWeb.R
alias Berrypod.Orders.OrderNotifier
alias Berrypod.Pages
alias BerrypodWeb.OrderLookupController
@@ -21,7 +22,7 @@ defmodule BerrypodWeb.Shop.Pages.Contact do
:page_description,
"Get in touch with us for any questions or help with your order."
)
|> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/contact")
|> assign(:og_url, R.url(R.contact()))
|> assign(:tracking_state, :idle)
|> assign(:page, page)
@@ -54,7 +55,7 @@ defmodule BerrypodWeb.Shop.Pages.Contact do
{:noreply,
socket
|> put_flash(:info, "Message sent! We'll get back to you soon.")
|> push_navigate(to: "/contact")}
|> push_navigate(to: R.contact())}
{:error, :invalid_params} ->
{:noreply, put_flash(socket, :error, "Please fill in all required fields.")}

View File

@@ -9,6 +9,7 @@ defmodule BerrypodWeb.Shop.Pages.Content do
alias Berrypod.LegalPages
alias Berrypod.Pages
alias Berrypod.Theme.PreviewData
alias BerrypodWeb.R
def init(socket, _params, _uri) do
# Content pages load in handle_params based on live_action
@@ -38,7 +39,7 @@ defmodule BerrypodWeb.Shop.Pages.Content do
%{
page_title: "About",
page_description: "Your story goes here this is sample content for the demo shop",
og_url: BerrypodWeb.Endpoint.url() <> "/about"
og_url: R.url(R.about())
},
PreviewData.about_content()
}
@@ -49,7 +50,7 @@ defmodule BerrypodWeb.Shop.Pages.Content do
%{
page_title: "Delivery & returns",
page_description: "Everything you need to know about shipping and returns.",
og_url: BerrypodWeb.Endpoint.url() <> "/delivery"
og_url: R.url(R.delivery())
},
LegalPages.delivery_content()
}
@@ -60,7 +61,7 @@ defmodule BerrypodWeb.Shop.Pages.Content do
%{
page_title: "Privacy policy",
page_description: "How we handle your personal information.",
og_url: BerrypodWeb.Endpoint.url() <> "/privacy"
og_url: R.url(R.privacy())
},
LegalPages.privacy_content()
}
@@ -71,7 +72,7 @@ defmodule BerrypodWeb.Shop.Pages.Content do
%{
page_title: "Terms of service",
page_description: "The terms and conditions governing purchases from our shop.",
og_url: BerrypodWeb.Endpoint.url() <> "/terms"
og_url: R.url(R.terms())
},
LegalPages.terms_content()
}

View File

@@ -7,6 +7,7 @@ defmodule BerrypodWeb.Shop.Pages.OrderDetail do
import Phoenix.LiveView, only: [push_navigate: 2]
alias Berrypod.{Orders, Pages}
alias BerrypodWeb.R
alias Berrypod.Products
alias Berrypod.Products.ProductImage
@@ -54,7 +55,7 @@ defmodule BerrypodWeb.Shop.Pages.OrderDetail do
{:noreply, socket}
else
{:noreply, push_navigate(socket, to: "/orders")}
{:noreply, push_navigate(socket, to: R.orders())}
end
end

View File

@@ -7,6 +7,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
import Phoenix.LiveView, only: [connected?: 1, push_navigate: 2]
alias Berrypod.{Analytics, Cart, Pages}
alias BerrypodWeb.R
alias Berrypod.Images.Optimizer
alias Berrypod.Products
alias Berrypod.Products.{Product, ProductImage}
@@ -15,11 +16,11 @@ defmodule BerrypodWeb.Shop.Pages.Product do
# Try to get product by slug, including discontinued products
case Products.get_product_by_slug(slug, preload: [:variants, :images]) do
nil ->
{:noreply, push_navigate(socket, to: "/collections/all")}
{:noreply, push_navigate(socket, to: R.collection("all"))}
%{visible: false, status: status} when status != "discontinued" ->
# Hidden but not discontinued - redirect away
{:noreply, push_navigate(socket, to: "/collections/all")}
{:noreply, push_navigate(socket, to: R.collection("all"))}
product ->
all_images =
@@ -42,12 +43,12 @@ defmodule BerrypodWeb.Shop.Pages.Product do
if connected?(socket) and socket.assigns[:analytics_visitor_hash] do
Analytics.track_event(
"product_view",
Map.put(BerrypodWeb.AnalyticsHook.attrs(socket), :pathname, "/products/#{slug}")
Map.put(BerrypodWeb.AnalyticsHook.attrs(socket), :pathname, R.product(slug))
)
end
base = BerrypodWeb.Endpoint.url()
og_url = base <> "/products/#{slug}"
og_url = R.url(R.product(slug))
og_image = og_image_url(all_images)
page = Pages.get_page("pdp")
@@ -107,7 +108,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
Map.put(
BerrypodWeb.AnalyticsHook.attrs(socket),
:pathname,
"/products/#{socket.assigns.product.slug}"
R.product(socket.assigns.product.slug)
)
)
end
@@ -204,7 +205,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
opt_type.values
|> Enum.map(fn value ->
params = Map.put(selected_options, opt_type.name, value.title)
{value.title, "/products/#{slug}?#{URI.encode_query(params)}"}
{value.title, R.product(slug) <> "?#{URI.encode_query(params)}"}
end)
|> Map.new()
@@ -278,7 +279,7 @@ defmodule BerrypodWeb.Shop.Pages.Product do
# ── JSON-LD and meta helpers ─────────────────────────────────────────
defp product_json_ld(product, url, image, base) do
defp product_json_ld(product, url, image, _base) do
category_slug =
if product.category,
do: product.category |> String.downcase() |> String.replace(" ", "-"),
@@ -286,13 +287,13 @@ defmodule BerrypodWeb.Shop.Pages.Product do
breadcrumbs =
[
%{"@type" => "ListItem", "position" => 1, "name" => "Home", "item" => base <> "/"},
%{"@type" => "ListItem", "position" => 1, "name" => "Home", "item" => R.url(R.home())},
product.category &&
%{
"@type" => "ListItem",
"position" => 2,
"name" => product.category,
"item" => base <> "/collections/#{category_slug}"
"item" => R.url(R.collection(category_slug))
},
%{
"@type" => "ListItem",

View File

@@ -7,6 +7,7 @@ defmodule BerrypodWeb.Shop.Pages.Search do
import Phoenix.LiveView, only: [push_patch: 2]
alias Berrypod.{Pages, Search}
alias BerrypodWeb.R
def init(socket, _params, _uri) do
page = Pages.get_page("search")
@@ -32,7 +33,7 @@ defmodule BerrypodWeb.Shop.Pages.Search do
end
def handle_event("search_submit", %{"q" => query}, socket) do
{:noreply, push_patch(socket, to: "/search?q=#{query}")}
{:noreply, push_patch(socket, to: R.search() <> "?q=#{query}")}
end
def handle_event(_event, _params, _socket), do: :cont

View File

@@ -270,10 +270,11 @@ defmodule BerrypodWeb.PageEditorHook do
end
# Initialize settings form from current page if not already set
# Only custom pages have editable settings (meta_description, published, etc.)
# Init settings form for pages with editable settings
# Custom pages: all settings editable
# System pages: only url_slug editable (except home)
socket =
if is_nil(socket.assigns[:settings_form]) && socket.assigns[:page] &&
socket.assigns.page[:type] == "custom" do
if is_nil(socket.assigns[:settings_form]) && socket.assigns[:page] do
init_settings_form(socket)
else
socket
@@ -930,14 +931,14 @@ defmodule BerrypodWeb.PageEditorHook do
# Catch-all for unknown theme actions
defp handle_theme_action(_action, _params, socket), do: {:halt, socket}
# ── Settings editing actions (custom page settings) ────────────────
# ── Settings editing actions (page settings) ────────────────
# Validate page settings form (live as-you-type)
defp handle_settings_action("validate_page", %{"page" => params}, socket) do
page = socket.assigns.page
# Only allow editing custom pages
if page && page.type == "custom" do
# Allow editing for custom pages (all fields) or system pages (url_slug only)
if page && page.type in ["custom", "system"] do
socket =
socket
|> assign(:settings_form, params)
@@ -954,43 +955,127 @@ defmodule BerrypodWeb.PageEditorHook do
defp handle_settings_action("save_page", %{"page" => params}, socket) do
page = socket.assigns.page
# Only allow editing custom pages
if page && page.type == "custom" do
# Normalize checkbox fields (unchecked checkboxes aren't sent)
params =
params
|> Map.put_new("published", "false")
|> Map.put_new("show_in_nav", "false")
cond do
# Custom pages: save all settings including url_slug
page && page.type == "custom" ->
save_custom_page_settings(socket, page, params)
old_slug = page.slug
# System pages with url_prefix (collections, products, orders)
page && page.type == "system" && params["url_prefix"] ->
save_system_page_url_prefix(socket, page, params["url_prefix"])
# Fetch the Page struct from DB (assigns.page may be a map from cache)
page_struct = Pages.get_page_struct(page.slug)
# System pages: only save url_slug (other fields are read-only)
page && page.type == "system" && params["url_slug"] ->
save_system_page_url_slug(socket, page, params["url_slug"])
case Pages.update_custom_page(page_struct, params) do
{:ok, updated_page} ->
# Reinitialize form from saved page
true ->
{:halt, socket}
end
end
defp handle_settings_action(_action, _params, socket), do: {:halt, socket}
defp save_custom_page_settings(socket, page, params) do
# Normalize checkbox fields (unchecked checkboxes aren't sent)
params =
params
|> Map.put_new("published", "false")
|> Map.put_new("show_in_nav", "false")
old_slug = page.slug
# Fetch the Page struct from DB (assigns.page may be a map from cache)
page_struct = Pages.get_page_struct(page.slug)
# Handle url_slug change separately to ensure redirects are created
url_slug = params["url_slug"]
old_url_slug = page[:url_slug]
# First update the main page settings
case Pages.update_custom_page(page_struct, params) do
{:ok, updated_page} ->
# If url_slug changed, handle redirect creation
socket =
if url_slug != old_url_slug do
Pages.update_page_url_slug(updated_page, url_slug)
# Reload page to get updated url_slug
updated_page = Pages.get_page(updated_page.slug)
assign(socket, :page, updated_page)
else
assign(socket, :page, updated_page)
end
socket =
socket
|> assign(:settings_form, nil)
|> assign(:settings_dirty, false)
|> assign(:settings_save_status, :saved)
# Reinit form with new page values
socket = init_settings_form(socket)
# If slug changed, redirect to new URL
socket =
if updated_page.slug != old_slug do
push_navigate(socket, to: "/#{updated_page.slug}")
else
socket
end
{:halt, socket}
{:error, _changeset} ->
socket = assign(socket, :settings_save_status, :error)
{:halt, socket}
end
end
defp save_system_page_url_slug(socket, page, url_slug) do
case Pages.update_page_url_slug(page.slug, url_slug) do
{:ok, _updated} ->
# Reload page from cache to get updated data
updated_page = Pages.get_page(page.slug)
socket =
socket
|> assign(:page, updated_page)
|> assign(:settings_form, nil)
|> assign(:settings_dirty, false)
|> assign(:settings_save_status, :saved)
socket = init_settings_form(socket)
{:halt, socket}
{:error, _} ->
socket = assign(socket, :settings_save_status, :error)
{:halt, socket}
end
end
# Save URL prefix for prefixed pages (collection, pdp, orders)
defp save_system_page_url_prefix(socket, page, new_prefix) do
# Determine the prefix type based on the page slug
prefix_type =
case page.slug do
"collection" -> :collections
"pdp" -> :products
"orders" -> :orders
"order_detail" -> :orders
_ -> nil
end
if prefix_type do
case Settings.update_url_prefix(prefix_type, new_prefix) do
{:ok, _} ->
socket =
socket
|> assign(:page, updated_page)
|> assign(:settings_form, nil)
|> assign(:settings_dirty, false)
|> assign(:settings_save_status, :saved)
# Reinit form with new page values
socket = init_settings_form(socket)
# If slug changed, redirect to new URL
socket =
if updated_page.slug != old_slug do
push_navigate(socket, to: "/#{updated_page.slug}")
else
socket
end
{:halt, socket}
{:error, _changeset} ->
{:error, _reason} ->
socket = assign(socket, :settings_save_status, :error)
{:halt, socket}
end
@@ -999,9 +1084,6 @@ defmodule BerrypodWeb.PageEditorHook do
end
end
# Catch-all for unknown settings actions
defp handle_settings_action(_action, _params, socket), do: {:halt, socket}
# --- Site tab event handlers ---
defp handle_site_action("update", %{"site" => site_params}, socket) do
@@ -1266,13 +1348,33 @@ defmodule BerrypodWeb.PageEditorHook do
defp has_settings_changed?(page, params) do
page.title != (params["title"] || "") or
page.slug != (params["slug"] || "") or
(page[:url_slug] || "") != (params["url_slug"] || "") or
(page.meta_description || "") != (params["meta_description"] || "") or
to_string(page.published) != (params["published"] || "false") or
to_string(page.show_in_nav) != (params["show_in_nav"] || "false") or
(page.nav_label || "") != (params["nav_label"] || "") or
to_string(page.nav_position || 0) != (params["nav_position"] || "0")
to_string(page.nav_position || 0) != (params["nav_position"] || "0") or
has_prefix_changed?(page, params)
end
# Check if URL prefix changed for prefixed pages
defp has_prefix_changed?(page, params) do
case params["url_prefix"] do
nil ->
false
new_prefix ->
prefix_type = prefix_type_for_page(page.slug)
prefix_type != nil and BerrypodWeb.R.prefix(prefix_type) != new_prefix
end
end
defp prefix_type_for_page("collection"), do: :collections
defp prefix_type_for_page("pdp"), do: :products
defp prefix_type_for_page("orders"), do: :orders
defp prefix_type_for_page("order_detail"), do: :orders
defp prefix_type_for_page(_), do: nil
# Initialize settings form from page values
defp init_settings_form(socket) do
page = socket.assigns.page
@@ -1280,6 +1382,7 @@ defmodule BerrypodWeb.PageEditorHook do
form = %{
"title" => page.title || "",
"slug" => page.slug || "",
"url_slug" => page[:url_slug] || "",
"meta_description" => page.meta_description || "",
"published" => to_string(page.published),
"show_in_nav" => to_string(page.show_in_nav),
@@ -1506,6 +1609,15 @@ defmodule BerrypodWeb.PageEditorHook do
defp save_all_tabs(socket) do
socket
|> maybe_save_page()
|> maybe_save_settings()
|> maybe_finish_save()
end
# If settings save triggered a navigation, don't overwrite the socket
defp maybe_finish_save(%{redirected: {:live, _, _}} = socket), do: socket
defp maybe_finish_save(socket) do
socket
|> maybe_save_theme()
|> maybe_save_site()
|> assign(:editor_save_status, :saved)
@@ -1537,6 +1649,124 @@ defmodule BerrypodWeb.PageEditorHook do
end
end
defp maybe_save_settings(socket) do
if socket.assigns[:settings_dirty] do
page = socket.assigns.page
params = socket.assigns[:settings_form] || %{}
cond do
# Custom pages: save all settings including url_slug
page && page.type == "custom" ->
do_save_custom_page_settings(socket, page, params)
# System pages with url_prefix (collections, products, orders)
page && page.type == "system" && params["url_prefix"] ->
do_save_system_page_url_prefix(socket, page, params["url_prefix"])
# System pages: only save url_slug
page && page.type == "system" && params["url_slug"] ->
do_save_system_page_url_slug(socket, page, params["url_slug"])
true ->
socket
end
else
socket
end
end
defp do_save_custom_page_settings(socket, page, params) do
params =
params
|> Map.put_new("published", "false")
|> Map.put_new("show_in_nav", "false")
page_struct = Pages.get_page_struct(page.slug)
url_slug = params["url_slug"]
old_url_slug = page[:url_slug]
case Pages.update_custom_page(page_struct, params) do
{:ok, updated_page} ->
if url_slug != old_url_slug do
Pages.update_page_url_slug(updated_page, url_slug)
end
updated_page = Pages.get_page(updated_page.slug)
socket
|> assign(:page, updated_page)
|> assign(:settings_form, nil)
|> assign(:settings_dirty, false)
|> init_settings_form()
{:error, _changeset} ->
socket
end
end
defp do_save_system_page_url_slug(socket, page, url_slug) do
old_url = Pages.Page.effective_url(page)
case Pages.update_page_url_slug(page.slug, url_slug) do
{:ok, _updated} ->
updated_page = Pages.get_page(page.slug)
new_url = Pages.Page.effective_url(updated_page)
socket =
socket
|> assign(:page, updated_page)
|> assign(:settings_form, nil)
|> assign(:settings_dirty, false)
|> init_settings_form()
# If URL changed, navigate to the new URL
if old_url != new_url do
push_navigate(socket, to: "/#{new_url}?edit=page")
else
socket
end
{:error, _reason} ->
socket
end
end
defp do_save_system_page_url_prefix(socket, page, new_prefix) do
prefix_type = prefix_type_for_page(page.slug)
if prefix_type do
old_prefix = BerrypodWeb.R.prefix(prefix_type)
case Settings.update_url_prefix(prefix_type, new_prefix) do
{:ok, _} ->
socket =
socket
|> assign(:settings_form, nil)
|> assign(:settings_dirty, false)
# If prefix changed, navigate to the new URL
if old_prefix != new_prefix do
# For collection page, navigate to /new-prefix/all
new_path =
case page.slug do
"collection" -> "/#{new_prefix}/all?edit=page"
"pdp" -> "/#{new_prefix}?edit=page"
_ -> "/#{new_prefix}?edit=page"
end
push_navigate(socket, to: new_path)
else
socket
end
{:error, _reason} ->
socket
end
else
socket
end
end
defp maybe_save_theme(socket) do
if socket.assigns[:theme_dirty] do
case Settings.update_theme_settings(Map.from_struct(socket.assigns.theme_editor_settings)) do

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

View File

@@ -18,7 +18,7 @@ defmodule BerrypodWeb.Plugs.BrokenUrlTracker do
def call(conn, {router, router_opts}) do
router.call(conn, router_opts)
rescue
e in Phoenix.Router.NoRouteError ->
e in [Phoenix.Router.NoRouteError, BerrypodWeb.NotFoundError] ->
unless static_path?(conn.request_path) do
prior_hits = Berrypod.Analytics.count_pageviews_for_path(conn.request_path)
Berrypod.Redirects.record_broken_url(conn.request_path, prior_hits)