consolidate shop pages into unified LiveView for editor state persistence
All checks were successful
deploy / deploy (push) Successful in 1m27s

Replace individual shop LiveViews with a single Shop.Page that dispatches
to page modules based on live_action. This enables patch navigation between
pages, preserving socket state (including editor state) across transitions.

Changes:
- Add Shop.Page unified LiveView with handle_params dispatch
- Extract page logic into Shop.Pages.* modules (Home, Product, Collection, etc.)
- Update router to use Shop.Page with live_action for all shop routes
- Change navigate= to patch= in shop component links
- Add maybe_sync_editing_blocks to reload editor state when page changes
- Track editor_page_slug to detect cross-page navigation while editing
- Fix picture element height when hover image disabled
- Extract ThemeEditor components for shared use

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-09 14:47:50 +00:00
parent ae0a149ecd
commit bb5d220079
29 changed files with 1410 additions and 1037 deletions

View File

@@ -178,7 +178,7 @@ defmodule BerrypodWeb.PageRenderer do
"""
end
# Theme editor content - shows theme controls
# Theme editor content - uses shared component
attr :theme_editor_settings, :map, default: nil
attr :theme_editor_active_preset, :atom, default: nil
attr :theme_editor_presets, :list, default: []
@@ -187,131 +187,14 @@ defmodule BerrypodWeb.PageRenderer do
defp theme_editor_content(assigns) do
~H"""
<div class="editor-theme-content">
<%= if @theme_editor_settings do %>
<%!-- Shop name --%>
<div class="theme-section">
<label class="theme-section-label">Shop name</label>
<form phx-change="theme_update_setting" phx-value-field="site_name">
<input
type="text"
name="site_name"
value={@site_name}
placeholder="Your shop name"
class="admin-input"
/>
</form>
</div>
<%!-- Presets --%>
<div class="theme-section">
<label class="theme-section-label">Preset</label>
<div class="theme-presets">
<%= for {preset_name, description} <- @theme_editor_presets do %>
<button
type="button"
phx-click="theme_apply_preset"
phx-value-preset={preset_name}
class={[
"theme-preset",
@theme_editor_active_preset == preset_name && "theme-preset-active"
]}
>
<div class="theme-preset-name">{preset_name}</div>
<div class="theme-preset-desc">{description}</div>
</button>
<% end %>
</div>
</div>
<%!-- Mood --%>
<div class="theme-section">
<label class="theme-section-label">Colour mood</label>
<div class="theme-chips">
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
<button
type="button"
phx-click="theme_update_setting"
phx-value-field="mood"
phx-value-setting_value={mood}
class={["theme-chip", @theme_editor_settings.mood == mood && "theme-chip-active"]}
>
{mood}
</button>
<% end %>
</div>
</div>
<%!-- Typography --%>
<div class="theme-section">
<label class="theme-section-label">Font style</label>
<div class="theme-chips">
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
<button
type="button"
phx-click="theme_update_setting"
phx-value-field="typography"
phx-value-setting_value={typo}
class={[
"theme-chip",
@theme_editor_settings.typography == typo && "theme-chip-active"
]}
>
{typo}
</button>
<% end %>
</div>
</div>
<%!-- Shape --%>
<div class="theme-section">
<label class="theme-section-label">Corner style</label>
<div class="theme-chips">
<%= for shape <- ["sharp", "soft", "round", "pill"] do %>
<button
type="button"
phx-click="theme_update_setting"
phx-value-field="shape"
phx-value-setting_value={shape}
class={["theme-chip", @theme_editor_settings.shape == shape && "theme-chip-active"]}
>
{shape}
</button>
<% end %>
</div>
</div>
<%!-- More options link --%>
<details
class="theme-customise"
id="theme-customise-section"
open={@theme_editor_customise_open}
>
<summary class="theme-customise-summary" phx-click="theme_toggle_customise">
<span class="theme-customise-label">More options</span>
<svg
class="theme-customise-chevron"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</summary>
<div class="theme-customise-body">
<p class="admin-text-secondary">
For full theme customisation including branding, colours, and layout, <a
href="/admin/theme"
class="admin-link"
>visit the theme editor</a>.
</p>
</div>
</details>
<% else %>
<p class="admin-text-secondary">Loading theme settings...</p>
<% end %>
</div>
<.compact_editor
theme_settings={@theme_editor_settings}
active_preset={@theme_editor_active_preset}
presets={@theme_editor_presets}
site_name={@site_name}
customise_open={@theme_editor_customise_open}
event_prefix="theme_"
/>
"""
end
@@ -727,7 +610,7 @@ defmodule BerrypodWeb.PageRenderer do
<ul class="collection-filter-pills">
<li>
<.link
navigate={collection_path("all", @current_sort)}
patch={collection_path("all", @current_sort)}
aria-current={@current_slug == nil && "page"}
class={["collection-filter-pill", @current_slug == nil && "active"]}
>
@@ -736,7 +619,7 @@ defmodule BerrypodWeb.PageRenderer do
</li>
<li>
<.link
navigate={collection_path("sale", @current_sort)}
patch={collection_path("sale", @current_sort)}
aria-current={@current_slug == "sale" && "page"}
class={["collection-filter-pill", @current_slug == "sale" && "active"]}
>
@@ -746,7 +629,7 @@ defmodule BerrypodWeb.PageRenderer do
<%= for category <- assigns[:categories] || [] do %>
<li>
<.link
navigate={collection_path(category.slug, @current_sort)}
patch={collection_path(category.slug, @current_sort)}
aria-current={@current_slug == category.slug && "page"}
class={["collection-filter-pill", @current_slug == category.slug && "active"]}
>
@@ -813,7 +696,7 @@ defmodule BerrypodWeb.PageRenderer do
<%= 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">
<.link patch={~p"/collections/all"} class="collection-empty-link">
View all products
</.link>
</div>
@@ -1020,7 +903,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 navigate="/contact" class="checkout-contact-link">contact us</.link>.
If this page doesn't update, please <.link patch="/contact" class="checkout-contact-link">contact us</.link>.
</p>
</div>
<% end %>
@@ -1045,20 +928,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 navigate="/contact">contact page</.link> to request a new one.
Head back to the <.link patch="/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 navigate="/contact">get in touch</.link>.
If something doesn't look right, <.link patch="/contact">get in touch</.link>.
</p>
</div>
<% true -> %>
<div class="orders-list">
<%= for order <- assigns[:orders] do %>
<.link navigate={"/orders/#{order.order_number}"} class="order-summary-card">
<.link patch={"/orders/#{order.order_number}"} class="order-summary-card">
<div class="order-summary-top">
<div>
<p class="order-summary-number">{order.order_number}</p>
@@ -1100,7 +983,7 @@ defmodule BerrypodWeb.PageRenderer do
~H"""
<%= if assigns[:order] do %>
<div class="order-detail-header">
<.link navigate="/orders" class="order-detail-back">&larr; Back to orders</.link>
<.link patch="/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"}>
@@ -1155,7 +1038,7 @@ defmodule BerrypodWeb.PageRenderer do
<div>
<%= if info && info.slug do %>
<.link
navigate={"/products/#{info.slug}"}
patch={"/products/#{info.slug}"}
class="checkout-item-name checkout-item-link"
>
{item.product_name}
@@ -1245,7 +1128,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 navigate="/collections/all" class="collection-empty-link">Browse all products</.link>
<.link patch="/collections/all" class="collection-empty-link">Browse all products</.link>
</div>
<% end %>
<% end %>
@@ -1285,7 +1168,7 @@ defmodule BerrypodWeb.PageRenderer do
~H"""
<div class="block-button" data-align={@alignment}>
<.link
navigate={@href}
patch={@href}
class={if @btn_style == "outline", do: "themed-button-outline", else: "themed-button"}
>
{@text}