From bb5d220079bdb60d76a0ccde508f7f53480f120e Mon Sep 17 00:00:00 2001
From: jamey
Date: Mon, 9 Mar 2026 14:47:50 +0000
Subject: [PATCH] consolidate shop pages into unified LiveView for editor state
persistence
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
---
assets/css/admin/components.css | 7 +
assets/css/shop/components.css | 3 +-
.../components/shop_components.ex | 2 +
.../components/shop_components/base.ex | 4 +-
.../components/shop_components/cart.ex | 6 +-
.../components/shop_components/layout.ex | 18 +-
.../components/shop_components/product.ex | 22 +-
.../shop_components/theme_editor.ex | 731 ++++++++++++++++++
.../live/admin/theme/index.html.heex | 580 +-------------
lib/berrypod_web/live/shop/cart.ex | 20 -
.../live/shop/checkout_success.ex | 58 --
lib/berrypod_web/live/shop/coming_soon.ex | 5 +
lib/berrypod_web/live/shop/page.ex | 154 ++++
lib/berrypod_web/live/shop/pages/cart.ex | 31 +
.../live/shop/pages/checkout_success.ex | 82 ++
.../live/shop/{ => pages}/collection.ex | 81 +-
.../live/shop/{ => pages}/contact.ex | 52 +-
.../live/shop/{ => pages}/content.ex | 30 +-
.../live/shop/{ => pages}/custom_page.ex | 22 +-
.../live/shop/{ => pages}/home.ex | 22 +-
.../live/shop/{ => pages}/order_detail.ex | 36 +-
.../live/shop/{ => pages}/orders.ex | 25 +-
.../{product_show.ex => pages/product.ex} | 113 ++-
lib/berrypod_web/live/shop/pages/search.ex | 39 +
lib/berrypod_web/live/shop/search.ex | 34 -
lib/berrypod_web/page_editor_hook.ex | 79 +-
lib/berrypod_web/page_renderer.ex | 159 +---
lib/berrypod_web/router.ex | 28 +-
.../live/shop/search_integration_test.exs | 4 +-
29 files changed, 1410 insertions(+), 1037 deletions(-)
create mode 100644 lib/berrypod_web/components/shop_components/theme_editor.ex
delete mode 100644 lib/berrypod_web/live/shop/cart.ex
delete mode 100644 lib/berrypod_web/live/shop/checkout_success.ex
create mode 100644 lib/berrypod_web/live/shop/page.ex
create mode 100644 lib/berrypod_web/live/shop/pages/cart.ex
create mode 100644 lib/berrypod_web/live/shop/pages/checkout_success.ex
rename lib/berrypod_web/live/shop/{ => pages}/collection.ex (63%)
rename lib/berrypod_web/live/shop/{ => pages}/contact.ex (58%)
rename lib/berrypod_web/live/shop/{ => pages}/content.ex (69%)
rename lib/berrypod_web/live/shop/{ => pages}/custom_page.ex (78%)
rename lib/berrypod_web/live/shop/{ => pages}/home.ex (63%)
rename lib/berrypod_web/live/shop/{ => pages}/order_detail.ex (65%)
rename lib/berrypod_web/live/shop/{ => pages}/orders.ex (53%)
rename lib/berrypod_web/live/shop/{product_show.ex => pages/product.ex} (92%)
create mode 100644 lib/berrypod_web/live/shop/pages/search.ex
delete mode 100644 lib/berrypod_web/live/shop/search.ex
diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css
index 38078bf..54f882b 100644
--- a/assets/css/admin/components.css
+++ b/assets/css/admin/components.css
@@ -2534,6 +2534,13 @@
cursor: pointer;
}
+/* On larger screens, hide the overlay so you can navigate while editing */
+@media (min-width: 768px) {
+ .editor-overlay {
+ display: none;
+ }
+}
+
/* ── Editor panel ── */
.editor-panel {
position: fixed;
diff --git a/assets/css/shop/components.css b/assets/css/shop/components.css
index 24f4a88..de0ace7 100644
--- a/assets/css/shop/components.css
+++ b/assets/css/shop/components.css
@@ -57,7 +57,8 @@
background-color: #e5e7eb;
}
- .product-card-image-wrap img {
+ .product-card-image-wrap img,
+ .product-card-image-wrap picture {
width: 100%;
height: 100%;
object-fit: cover;
diff --git a/lib/berrypod_web/components/shop_components.ex b/lib/berrypod_web/components/shop_components.ex
index 7771862..4215125 100644
--- a/lib/berrypod_web/components/shop_components.ex
+++ b/lib/berrypod_web/components/shop_components.ex
@@ -9,6 +9,7 @@ defmodule BerrypodWeb.ShopComponents do
- `Cart` — cart drawer, cart items, order summary
- `Product` — product cards, gallery, variant selector, hero sections
- `Content` — rich text, responsive images, contact form, reviews
+ - `ThemeEditor` — shared theme editor components for admin and on-site editing
"""
defmacro __using__(_opts \\ []) do
@@ -18,6 +19,7 @@ defmodule BerrypodWeb.ShopComponents do
import BerrypodWeb.ShopComponents.Content
import BerrypodWeb.ShopComponents.Layout
import BerrypodWeb.ShopComponents.Product
+ import BerrypodWeb.ShopComponents.ThemeEditor
end
end
end
diff --git a/lib/berrypod_web/components/shop_components/base.ex b/lib/berrypod_web/components/shop_components/base.ex
index 8f776ad..f7bf5dc 100644
--- a/lib/berrypod_web/components/shop_components/base.ex
+++ b/lib/berrypod_web/components/shop_components/base.ex
@@ -171,7 +171,7 @@ defmodule BerrypodWeb.ShopComponents.Base do
def shop_link_button(assigns) do
~H"""
<.link
- navigate={@href}
+ patch={@href}
class={["themed-button", @class]}
>
{render_slot(@inner_block)}
@@ -203,7 +203,7 @@ defmodule BerrypodWeb.ShopComponents.Base do
def shop_link_outline(assigns) do
~H"""
<.link
- navigate={@href}
+ patch={@href}
class={["themed-button-outline", @class]}
>
{render_slot(@inner_block)}
diff --git a/lib/berrypod_web/components/shop_components/cart.ex b/lib/berrypod_web/components/shop_components/cart.ex
index 7677560..a5b4146 100644
--- a/lib/berrypod_web/components/shop_components/cart.ex
+++ b/lib/berrypod_web/components/shop_components/cart.ex
@@ -178,7 +178,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
>
<%= if @mode != :preview do %>
<.link
- navigate={"/products/#{@item.product_id}"}
+ patch={"/products/#{@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}');"}
@@ -197,7 +197,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
<%= if @mode != :preview do %>
<.link
- navigate={"/products/#{@item.product_id}"}
+ patch={"/products/#{@item.product_id}"}
class="cart-item-name-link"
>
{@item.name}
@@ -296,7 +296,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
<% else %>
<.link
- navigate="/collections/all"
+ patch="/collections/all"
class="cart-continue-link"
>
Continue shopping
diff --git a/lib/berrypod_web/components/shop_components/layout.ex b/lib/berrypod_web/components/shop_components/layout.ex
index 9dcd9ae..14ce602 100644
--- a/lib/berrypod_web/components/shop_components/layout.ex
+++ b/lib/berrypod_web/components/shop_components/layout.ex
@@ -266,7 +266,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<% else %>
<.link
- navigate={@href}
+ patch={@href}
class="mobile-nav-link"
aria-current={if @is_current, do: "page", else: nil}
>
@@ -484,7 +484,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
aria-selected="false"
>
<.link
- navigate={"/products/#{item.product.slug || item.product.id}"}
+ patch={"/products/#{item.product.slug || item.product.id}"}
class="search-result"
phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")}
>
@@ -588,7 +588,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<% else %>
<.link
- navigate={item["href"]}
+ patch={item["href"]}
class="mobile-nav-link"
aria-current={@active_page in (item["active_slugs"] || [item["slug"]]) && "page"}
phx-click={Phoenix.LiveView.JS.dispatch("close-mobile-nav", to: "#mobile-nav-drawer")}
@@ -615,7 +615,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<% else %>
<.link
- navigate={"/collections/#{category.slug}"}
+ patch={"/collections/#{category.slug}"}
class="mobile-nav-link"
phx-click={
Phoenix.LiveView.JS.dispatch("close-mobile-nav", to: "#mobile-nav-drawer")
@@ -700,7 +700,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<% else %>
<.link
- navigate="/collections/all"
+ patch="/collections/all"
class="footer-link"
>
All products
@@ -709,7 +709,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<%= for category <- @categories do %>
<.link
- navigate={"/collections/#{category.slug}"}
+ patch={"/collections/#{category.slug}"}
class="footer-link"
>
{category.name}
@@ -735,7 +735,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
{item["label"]}
<% else %>
- <.link navigate={item["href"]} class="footer-link">
+ <.link patch={item["href"]} class="footer-link">
{item["label"]}
<% end %>
@@ -929,7 +929,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
/>
<% else %>
- <.link navigate="/" class="shop-logo-link">
+ <.link patch="/" class="shop-logo-link">
<.logo_inner
theme_settings={@theme_settings}
site_name={@site_name}
@@ -1015,7 +1015,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
{@label}
<% else %>
- <.link navigate={@href} class="nav-link">
+ <.link patch={@href} class="nav-link">
{@label}
<% end %>
diff --git a/lib/berrypod_web/components/shop_components/product.ex b/lib/berrypod_web/components/shop_components/product.ex
index cd51ffd..f2debc8 100644
--- a/lib/berrypod_web/components/shop_components/product.ex
+++ b/lib/berrypod_web/components/shop_components/product.ex
@@ -157,7 +157,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
<% else %>
<.link
- navigate={"/collections/#{Slug.slugify(@product.category)}"}
+ patch={"/collections/#{Slug.slugify(@product.category)}"}
class="product-card-category"
>
{@product.category}
@@ -177,7 +177,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
<% else %>
<.link
- navigate={"/products/#{Map.get(@product, :slug) || Map.get(@product, :id)}"}
+ patch={"/products/#{Map.get(@product, :slug) || Map.get(@product, :id)}"}
class="stretched-link"
>
{@product.title}
@@ -205,7 +205,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
defp product_card_image_wrap(assigns) do
~H"""
<%= if @href do %>
- <.link navigate={@href} class="product-card-image-wrap" tabindex="-1" aria-hidden="true">
+ <.link patch={@href} class="product-card-image-wrap" tabindex="-1" aria-hidden="true">
{render_slot(@inner_block)}
<% else %>
@@ -571,7 +571,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
<% else %>
<.link
- navigate={@href || "/"}
+ patch={@href || "/"}
class={@cta_class}
>
{@text}
@@ -627,7 +627,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
<% else %>
<.link
- navigate={"/collections/#{category.slug}"}
+ patch={"/collections/#{category.slug}"}
class="category-card"
>
<% else %>
<.link
- navigate={@cta_href}
+ patch={@cta_href}
class="outline-button"
>
{@cta_text}
@@ -826,7 +826,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
<% else %>
<.link
- navigate={@link_href || "/"}
+ patch={@link_href || "/"}
class="accent-link"
>
{@link_text}
@@ -960,7 +960,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
{item.label}
<% else %>
- <.link navigate={item.href || "/"}>{item.label}
+ <.link patch={item.href || "/"}>{item.label}
<% end %>
<% end %>
@@ -1749,7 +1749,7 @@ defmodule BerrypodWeb.ShopComponents.Product do
diff --git a/lib/berrypod_web/live/shop/cart.ex b/lib/berrypod_web/live/shop/cart.ex
deleted file mode 100644
index f0a69b6..0000000
--- a/lib/berrypod_web/live/shop/cart.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule BerrypodWeb.Shop.Cart do
- use BerrypodWeb, :live_view
-
- alias Berrypod.{Cart, Pages}
-
- @impl true
- def mount(_params, _session, socket) do
- page = Pages.get_page("cart")
- {:ok, socket |> assign(:page_title, "Cart") |> assign(:page, page)}
- end
-
- @impl true
- def render(assigns) do
- assigns = assign(assigns, :cart_page_subtotal, Cart.calculate_subtotal(assigns.cart_items))
-
- ~H"""
-
- """
- end
-end
diff --git a/lib/berrypod_web/live/shop/checkout_success.ex b/lib/berrypod_web/live/shop/checkout_success.ex
deleted file mode 100644
index fe9aeb1..0000000
--- a/lib/berrypod_web/live/shop/checkout_success.ex
+++ /dev/null
@@ -1,58 +0,0 @@
-defmodule BerrypodWeb.Shop.CheckoutSuccess do
- use BerrypodWeb, :live_view
-
- alias Berrypod.{Analytics, Orders, Pages}
-
- @impl true
- def mount(%{"session_id" => session_id}, _session, socket) do
- order = Orders.get_order_by_stripe_session(session_id)
-
- # Subscribe to order status updates (webhook may arrive after redirect)
- if order && connected?(socket) do
- Phoenix.PubSub.subscribe(Berrypod.PubSub, "order:#{order.id}:status")
- end
-
- # Track purchase event
- if order && connected?(socket) && socket.assigns[:analytics_visitor_hash] do
- attrs =
- BerrypodWeb.AnalyticsHook.attrs(socket)
- |> Map.merge(%{pathname: "/checkout/success", revenue: order.total})
-
- Analytics.track_event("purchase", attrs)
- end
-
- # Clear the cart after successful checkout
- socket =
- if order && connected?(socket) do
- BerrypodWeb.CartHook.broadcast_and_update(socket, [])
- else
- socket
- end
-
- page = Pages.get_page("checkout_success")
-
- socket =
- socket
- |> assign(:page_title, "Order confirmed")
- |> assign(:order, order)
- |> assign(:page, page)
-
- {:ok, socket}
- end
-
- def mount(_params, _session, socket) do
- {:ok, redirect(socket, to: ~p"/")}
- end
-
- @impl true
- def handle_info({:order_paid, order}, socket) do
- {:noreply, assign(socket, :order, order)}
- end
-
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
-end
diff --git a/lib/berrypod_web/live/shop/coming_soon.ex b/lib/berrypod_web/live/shop/coming_soon.ex
index 0a07301..fd91dbe 100644
--- a/lib/berrypod_web/live/shop/coming_soon.ex
+++ b/lib/berrypod_web/live/shop/coming_soon.ex
@@ -6,6 +6,11 @@ defmodule BerrypodWeb.Shop.ComingSoon do
{:ok, assign(socket, :page_title, "Coming soon")}
end
+ @impl true
+ def handle_params(_params, _uri, socket) do
+ {:noreply, socket}
+ end
+
@impl true
def render(assigns) do
~H"""
diff --git a/lib/berrypod_web/live/shop/page.ex b/lib/berrypod_web/live/shop/page.ex
new file mode 100644
index 0000000..1562e61
--- /dev/null
+++ b/lib/berrypod_web/live/shop/page.ex
@@ -0,0 +1,154 @@
+defmodule BerrypodWeb.Shop.Page do
+ @moduledoc """
+ Unified shop LiveView that handles all shop pages.
+
+ Using a single LiveView enables `patch` navigation between pages,
+ preserving socket state (including editor state) across transitions.
+ """
+
+ use BerrypodWeb, :live_view
+
+ alias BerrypodWeb.Shop.Pages
+
+ # Map live_action atoms to page handler modules
+ @page_modules %{
+ home: Pages.Home,
+ product: Pages.Product,
+ collection: Pages.Collection,
+ cart: Pages.Cart,
+ contact: Pages.Contact,
+ search: Pages.Search,
+ orders: Pages.Orders,
+ order_detail: Pages.OrderDetail,
+ checkout_success: Pages.CheckoutSuccess,
+ custom_page: Pages.CustomPage,
+ # Content pages all use the same module
+ about: Pages.Content,
+ delivery: Pages.Content,
+ privacy: Pages.Content,
+ terms: Pages.Content
+ }
+
+ # Pages that need session data passed to init
+ @session_pages [:orders, :order_detail]
+
+ @impl true
+ def mount(_params, session, socket) do
+ # Store session for pages that need it (orders, order_detail)
+ {:ok, assign(socket, :_session, session)}
+ end
+
+ @impl true
+ def handle_params(params, uri, socket) do
+ action = socket.assigns.live_action
+ prev_action = socket.assigns[:_current_page_action]
+ module = @page_modules[action]
+
+ # Clean up previous page if needed (e.g., unsubscribe from PubSub)
+ socket = maybe_cleanup_previous_page(socket, prev_action)
+
+ socket =
+ if action != prev_action do
+ # Page type changed - call init
+ socket = assign(socket, :_current_page_action, action)
+
+ result =
+ if action in @session_pages do
+ module.init(socket, params, uri, socket.assigns._session)
+ else
+ module.init(socket, params, uri)
+ end
+
+ case result do
+ {:noreply, socket} -> socket
+ {:redirect, socket} -> socket
+ end
+ else
+ socket
+ end
+
+ # After page init, sync editor state if editing and page changed
+ socket = maybe_sync_editing_blocks(socket)
+
+ # Always call handle_params for URL changes
+ case module.handle_params(params, uri, socket) do
+ {:noreply, socket} -> {:noreply, socket}
+ end
+ end
+
+ # If editing and we navigated to a different page, reload editing_blocks
+ defp maybe_sync_editing_blocks(socket) do
+ page = socket.assigns[:page]
+ editing = socket.assigns[:editing]
+ editor_page_slug = socket.assigns[:editor_page_slug]
+
+ if editing && page && page.slug != editor_page_slug do
+ # Page changed while editing - reload editing state for the new page
+ allowed = Berrypod.Pages.BlockTypes.allowed_for(page.slug)
+ at_defaults = Berrypod.Pages.Defaults.matches_defaults?(page.slug, page.blocks)
+
+ socket
+ |> assign(:editing_blocks, page.blocks)
+ |> assign(:editor_page_slug, page.slug)
+ |> assign(:editor_dirty, false)
+ |> assign(:editor_at_defaults, at_defaults)
+ |> assign(:editor_history, [])
+ |> assign(:editor_future, [])
+ |> assign(:editor_expanded, MapSet.new())
+ |> assign(:editor_allowed_blocks, allowed)
+ else
+ socket
+ end
+ end
+
+ @impl true
+ def handle_event(event, params, socket) do
+ module = @page_modules[socket.assigns.live_action]
+
+ case module.handle_event(event, params, socket) do
+ :cont ->
+ # Event not handled by page module, let hooks handle it
+ {:noreply, socket}
+
+ {:noreply, socket} ->
+ {:noreply, socket}
+ end
+ end
+
+ @impl true
+ def handle_info(msg, socket) do
+ module = @page_modules[socket.assigns.live_action]
+
+ # Check if the module defines handle_info
+ if function_exported?(module, :handle_info, 2) do
+ case module.handle_info(msg, socket) do
+ :cont -> {:noreply, socket}
+ {:noreply, socket} -> {:noreply, socket}
+ end
+ else
+ {:noreply, socket}
+ end
+ end
+
+ @impl true
+ def render(assigns) do
+ # Cart page needs extra assigns computed at render time
+ assigns =
+ if assigns.live_action == :cart do
+ Pages.Cart.compute_assigns(assigns)
+ else
+ assigns
+ end
+
+ ~H"""
+
+ """
+ end
+
+ # Clean up previous page state when transitioning
+ defp maybe_cleanup_previous_page(socket, :checkout_success) do
+ Pages.CheckoutSuccess.cleanup(socket)
+ end
+
+ defp maybe_cleanup_previous_page(socket, _), do: socket
+end
diff --git a/lib/berrypod_web/live/shop/pages/cart.ex b/lib/berrypod_web/live/shop/pages/cart.ex
new file mode 100644
index 0000000..b006c9d
--- /dev/null
+++ b/lib/berrypod_web/live/shop/pages/cart.ex
@@ -0,0 +1,31 @@
+defmodule BerrypodWeb.Shop.Pages.Cart do
+ @moduledoc """
+ Cart page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 3]
+
+ alias Berrypod.Pages
+
+ def init(socket, _params, _uri) do
+ page = Pages.get_page("cart")
+
+ socket =
+ socket
+ |> assign(:page_title, "Cart")
+ |> assign(:page, page)
+
+ {:noreply, socket}
+ end
+
+ def handle_params(_params, _uri, socket) do
+ {:noreply, socket}
+ end
+
+ def handle_event(_event, _params, _socket), do: :cont
+
+ # Called from render to compute the subtotal
+ def compute_assigns(assigns) do
+ Map.put(assigns, :cart_page_subtotal, Berrypod.Cart.calculate_subtotal(assigns.cart_items))
+ end
+end
diff --git a/lib/berrypod_web/live/shop/pages/checkout_success.ex b/lib/berrypod_web/live/shop/pages/checkout_success.ex
new file mode 100644
index 0000000..80322a3
--- /dev/null
+++ b/lib/berrypod_web/live/shop/pages/checkout_success.ex
@@ -0,0 +1,82 @@
+defmodule BerrypodWeb.Shop.Pages.CheckoutSuccess do
+ @moduledoc """
+ Checkout success page handler for the unified Shop.Page LiveView.
+ Handles PubSub subscription for order status updates.
+ """
+
+ import Phoenix.Component, only: [assign: 3]
+ import Phoenix.LiveView, only: [connected?: 1, redirect: 2]
+
+ alias Berrypod.{Analytics, Orders, Pages}
+
+ def init(socket, %{"session_id" => session_id}, _uri) do
+ order = Orders.get_order_by_stripe_session(session_id)
+
+ # Subscribe to order status updates (webhook may arrive after redirect)
+ if order && connected?(socket) do
+ Phoenix.PubSub.subscribe(Berrypod.PubSub, "order:#{order.id}:status")
+ end
+
+ # Track purchase event
+ if order && connected?(socket) && socket.assigns[:analytics_visitor_hash] do
+ attrs =
+ BerrypodWeb.AnalyticsHook.attrs(socket)
+ |> Map.merge(%{pathname: "/checkout/success", revenue: order.total})
+
+ Analytics.track_event("purchase", attrs)
+ end
+
+ # Clear the cart after successful checkout
+ socket =
+ if order && connected?(socket) do
+ BerrypodWeb.CartHook.broadcast_and_update(socket, [])
+ else
+ socket
+ end
+
+ # Track subscription for cleanup when leaving this page
+ socket =
+ if order do
+ assign(socket, :checkout_order_subscription, "order:#{order.id}:status")
+ else
+ socket
+ end
+
+ page = Pages.get_page("checkout_success")
+
+ socket =
+ socket
+ |> assign(:page_title, "Order confirmed")
+ |> assign(:order, order)
+ |> assign(:page, page)
+
+ {:noreply, socket}
+ end
+
+ def init(socket, _params, _uri) do
+ {:redirect, redirect(socket, to: "/")}
+ end
+
+ def handle_params(_params, _uri, socket) do
+ {:noreply, socket}
+ end
+
+ def handle_event(_event, _params, _socket), do: :cont
+
+ def handle_info({:order_paid, order}, socket) do
+ {:noreply, assign(socket, :order, order)}
+ end
+
+ def handle_info(_msg, _socket), do: :cont
+
+ # Called when leaving this page to clean up subscription
+ def cleanup(socket) do
+ if topic = socket.assigns[:checkout_order_subscription] do
+ Phoenix.PubSub.unsubscribe(Berrypod.PubSub, topic)
+ end
+
+ socket
+ |> assign(:checkout_order_subscription, nil)
+ |> assign(:order, nil)
+ end
+end
diff --git a/lib/berrypod_web/live/shop/collection.ex b/lib/berrypod_web/live/shop/pages/collection.ex
similarity index 63%
rename from lib/berrypod_web/live/shop/collection.ex
rename to lib/berrypod_web/live/shop/pages/collection.ex
index 1b60559..2f287c2 100644
--- a/lib/berrypod_web/live/shop/collection.ex
+++ b/lib/berrypod_web/live/shop/pages/collection.ex
@@ -1,5 +1,10 @@
-defmodule BerrypodWeb.Shop.Collection do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.Collection do
+ @moduledoc """
+ Collection page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 3]
+ import Phoenix.LiveView, only: [push_patch: 2, push_navigate: 2, put_flash: 3]
alias Berrypod.{Pages, Pagination, Products}
@@ -12,8 +17,7 @@ defmodule BerrypodWeb.Shop.Collection do
{"name_desc", "Name: Z-A"}
]
- @impl true
- def mount(_params, _session, socket) do
+ def init(socket, _params, _uri) do
page = Pages.get_page("collection")
socket =
@@ -22,36 +26,52 @@ defmodule BerrypodWeb.Shop.Collection do
|> assign(:sort_options, @sort_options)
|> assign(:current_sort, "featured")
- {:ok, socket}
+ {:noreply, socket}
end
- @impl true
def handle_params(%{"slug" => slug} = params, _uri, socket) do
sort = params["sort"] || "featured"
page_num = Pagination.parse_page(params)
case load_collection(slug, sort, page_num) do
{:ok, title, category, pagination} ->
- {:noreply,
- socket
- |> assign(:page_title, title)
- |> assign(:page_description, collection_description(title))
- |> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/collections/#{slug}")
- |> assign(:collection_title, title)
- |> assign(:collection_slug, slug)
- |> assign(:current_category, category)
- |> assign(:current_sort, sort)
- |> assign(:pagination, pagination)
- |> assign(:products, pagination.items)}
+ socket =
+ socket
+ |> assign(:page_title, title)
+ |> assign(:page_description, collection_description(title))
+ |> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/collections/#{slug}")
+ |> assign(:collection_title, title)
+ |> assign(:collection_slug, slug)
+ |> assign(:current_category, category)
+ |> assign(:current_sort, sort)
+ |> assign(:pagination, pagination)
+ |> assign(:products, pagination.items)
+
+ {:noreply, socket}
:not_found ->
- {:noreply,
- socket
- |> put_flash(:error, "Collection not found")
- |> push_navigate(to: ~p"/collections/all")}
+ socket =
+ socket
+ |> put_flash(:error, "Collection not found")
+ |> push_navigate(to: "/collections/all")
+
+ {:noreply, socket}
end
end
+ def handle_event("sort_changed", %{"sort" => sort}, socket) do
+ slug =
+ case socket.assigns.current_category do
+ nil -> "all"
+ :sale -> "sale"
+ category -> category.slug
+ end
+
+ {:noreply, push_patch(socket, to: "/collections/#{slug}?sort=#{sort}")}
+ end
+
+ def handle_event(_event, _params, _socket), do: :cont
+
defp load_collection("all", sort, page) do
pagination = Products.list_visible_products_paginated(sort: sort, page: page)
{:ok, "All Products", nil, pagination}
@@ -79,26 +99,7 @@ defmodule BerrypodWeb.Shop.Collection do
end
end
- @impl true
- def handle_event("sort_changed", %{"sort" => sort}, socket) do
- slug =
- case socket.assigns.current_category do
- nil -> "all"
- :sale -> "sale"
- category -> category.slug
- end
-
- {:noreply, push_patch(socket, to: ~p"/collections/#{slug}?sort=#{sort}")}
- end
-
defp collection_description("All Products"), do: "Browse our full range of products."
defp collection_description("Sale"), do: "Browse our current sale items."
defp collection_description(title), do: "Browse our #{String.downcase(title)} collection."
-
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
end
diff --git a/lib/berrypod_web/live/shop/contact.ex b/lib/berrypod_web/live/shop/pages/contact.ex
similarity index 58%
rename from lib/berrypod_web/live/shop/contact.ex
rename to lib/berrypod_web/live/shop/pages/contact.ex
index a3be4e2..0d7a380 100644
--- a/lib/berrypod_web/live/shop/contact.ex
+++ b/lib/berrypod_web/live/shop/pages/contact.ex
@@ -1,28 +1,37 @@
-defmodule BerrypodWeb.Shop.Contact do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.Contact do
+ @moduledoc """
+ Contact page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 3]
+ import Phoenix.LiveView, only: [push_navigate: 2, put_flash: 3]
alias Berrypod.{ContactNotifier, Orders}
alias Berrypod.Orders.OrderNotifier
alias Berrypod.Pages
alias BerrypodWeb.OrderLookupController
- @impl true
- def mount(_params, _session, socket) do
+ def init(socket, _params, _uri) do
page = Pages.get_page("contact")
- {:ok,
- socket
- |> assign(:page_title, "Contact")
- |> assign(
- :page_description,
- "Get in touch with us for any questions or help with your order."
- )
- |> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/contact")
- |> assign(:tracking_state, :idle)
- |> assign(:page, page)}
+ socket =
+ socket
+ |> assign(:page_title, "Contact")
+ |> assign(
+ :page_description,
+ "Get in touch with us for any questions or help with your order."
+ )
+ |> assign(:og_url, BerrypodWeb.Endpoint.url() <> "/contact")
+ |> assign(:tracking_state, :idle)
+ |> assign(:page, page)
+
+ {:noreply, socket}
+ end
+
+ def handle_params(_params, _uri, socket) do
+ {:noreply, socket}
end
- @impl true
def handle_event("lookup_orders", %{"email" => email}, socket) do
orders = Orders.list_orders_by_email(email)
@@ -31,7 +40,7 @@ defmodule BerrypodWeb.Shop.Contact do
:not_found
else
token = OrderLookupController.generate_token(email)
- link = BerrypodWeb.Endpoint.url() <> ~p"/orders/verify/#{token}"
+ link = BerrypodWeb.Endpoint.url() <> "/orders/verify/#{token}"
OrderNotifier.deliver_order_lookup(email, link)
:sent
end
@@ -39,14 +48,13 @@ defmodule BerrypodWeb.Shop.Contact do
{:noreply, assign(socket, :tracking_state, state)}
end
- @impl true
def handle_event("send_contact", params, socket) do
case ContactNotifier.deliver_contact_message(params) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Message sent! We'll get back to you soon.")
- |> push_navigate(to: ~p"/contact")}
+ |> push_navigate(to: "/contact")}
{:error, :invalid_params} ->
{:noreply, put_flash(socket, :error, "Please fill in all required fields.")}
@@ -56,15 +64,9 @@ defmodule BerrypodWeb.Shop.Contact do
end
end
- @impl true
def handle_event("reset_tracking", _params, socket) do
{:noreply, assign(socket, :tracking_state, :idle)}
end
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
+ def handle_event(_event, _params, _socket), do: :cont
end
diff --git a/lib/berrypod_web/live/shop/content.ex b/lib/berrypod_web/live/shop/pages/content.ex
similarity index 69%
rename from lib/berrypod_web/live/shop/content.ex
rename to lib/berrypod_web/live/shop/pages/content.ex
index e4fa86c..7978453 100644
--- a/lib/berrypod_web/live/shop/content.ex
+++ b/lib/berrypod_web/live/shop/pages/content.ex
@@ -1,20 +1,25 @@
-defmodule BerrypodWeb.Shop.Content do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.Content do
+ @moduledoc """
+ Content page handler for the unified Shop.Page LiveView.
+ Handles about, delivery, privacy, and terms pages.
+ """
+
+ import Phoenix.Component, only: [assign: 2, assign: 3]
alias Berrypod.LegalPages
alias Berrypod.Pages
alias Berrypod.Theme.PreviewData
- @impl true
- def mount(_params, _session, socket) do
- {:ok, socket}
+ def init(socket, _params, _uri) do
+ # Content pages load in handle_params based on live_action
+ {:noreply, socket}
end
- @impl true
def handle_params(_params, _uri, socket) do
- slug = to_string(socket.assigns.live_action)
+ action = socket.assigns.live_action
+ slug = to_string(action)
page = Pages.get_page(slug)
- {seo, content_blocks} = page_config(socket.assigns.live_action)
+ {seo, content_blocks} = page_config(action)
socket =
socket
@@ -25,19 +30,14 @@ defmodule BerrypodWeb.Shop.Content do
{:noreply, socket}
end
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
+ def handle_event(_event, _params, _socket), do: :cont
# Returns {seo_assigns, content_blocks} for each content page
defp page_config(:about) do
{
%{
page_title: "About",
- page_description: "Your story goes here \u2013 this is sample content for the demo shop",
+ page_description: "Your story goes here – this is sample content for the demo shop",
og_url: BerrypodWeb.Endpoint.url() <> "/about"
},
PreviewData.about_content()
diff --git a/lib/berrypod_web/live/shop/custom_page.ex b/lib/berrypod_web/live/shop/pages/custom_page.ex
similarity index 78%
rename from lib/berrypod_web/live/shop/custom_page.ex
rename to lib/berrypod_web/live/shop/pages/custom_page.ex
index d624fa5..9dc40c0 100644
--- a/lib/berrypod_web/live/shop/custom_page.ex
+++ b/lib/berrypod_web/live/shop/pages/custom_page.ex
@@ -1,14 +1,17 @@
-defmodule BerrypodWeb.Shop.CustomPage do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.CustomPage do
+ @moduledoc """
+ Custom (CMS) page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 2, assign: 3]
alias Berrypod.Pages
- @impl true
- def mount(_params, _session, socket) do
- {:ok, socket}
+ def init(socket, _params, _uri) do
+ # Custom pages load in handle_params based on slug
+ {:noreply, socket}
end
- @impl true
def handle_params(%{"slug" => slug}, _uri, socket) do
page = Pages.get_page(slug)
@@ -38,12 +41,7 @@ defmodule BerrypodWeb.Shop.CustomPage do
end
end
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
+ def handle_event(_event, _params, _socket), do: :cont
defp record_broken_url(path) do
prior_hits = Berrypod.Analytics.count_pageviews_for_path(path)
diff --git a/lib/berrypod_web/live/shop/home.ex b/lib/berrypod_web/live/shop/pages/home.ex
similarity index 63%
rename from lib/berrypod_web/live/shop/home.ex
rename to lib/berrypod_web/live/shop/pages/home.ex
index 067cf81..dcbf4cf 100644
--- a/lib/berrypod_web/live/shop/home.ex
+++ b/lib/berrypod_web/live/shop/pages/home.ex
@@ -1,10 +1,13 @@
-defmodule BerrypodWeb.Shop.Home do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.Home do
+ @moduledoc """
+ Home page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 2, assign: 3]
alias Berrypod.Pages
- @impl true
- def mount(_params, _session, socket) do
+ def init(socket, _params, _uri) do
page = Pages.get_page("home")
extra = Pages.load_block_data(page.blocks, socket.assigns)
@@ -30,13 +33,12 @@ defmodule BerrypodWeb.Shop.Home do
|> assign(:page, page)
|> assign(extra)
- {:ok, socket}
+ {:noreply, socket}
end
- @impl true
- def render(assigns) do
- ~H"""
-
- """
+ def handle_params(_params, _uri, socket) do
+ {:noreply, socket}
end
+
+ def handle_event(_event, _params, _socket), do: :cont
end
diff --git a/lib/berrypod_web/live/shop/order_detail.ex b/lib/berrypod_web/live/shop/pages/order_detail.ex
similarity index 65%
rename from lib/berrypod_web/live/shop/order_detail.ex
rename to lib/berrypod_web/live/shop/pages/order_detail.ex
index ca8eb9b..91a5f24 100644
--- a/lib/berrypod_web/live/shop/order_detail.ex
+++ b/lib/berrypod_web/live/shop/pages/order_detail.ex
@@ -1,12 +1,16 @@
-defmodule BerrypodWeb.Shop.OrderDetail do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.OrderDetail do
+ @moduledoc """
+ Order detail page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 3]
+ import Phoenix.LiveView, only: [push_navigate: 2]
alias Berrypod.{Orders, Pages}
alias Berrypod.Products
alias Berrypod.Products.ProductImage
- @impl true
- def mount(_params, session, socket) do
+ def init(socket, _params, _uri, session) do
page = Pages.get_page("order_detail")
socket =
@@ -14,10 +18,9 @@ defmodule BerrypodWeb.Shop.OrderDetail do
|> assign(:lookup_email, session["order_lookup_email"])
|> assign(:page, page)
- {:ok, socket}
+ {:noreply, socket}
end
- @impl true
def handle_params(%{"order_number" => order_number}, _uri, socket) do
email = socket.assigns.lookup_email
@@ -43,20 +46,17 @@ defmodule BerrypodWeb.Shop.OrderDetail do
{id, %{thumb: thumb, slug: slug}}
end)
- {:noreply,
- socket
- |> assign(:page_title, "Order #{order_number}")
- |> assign(:order, order)
- |> assign(:thumbnails, thumbnails)}
+ socket =
+ socket
+ |> assign(:page_title, "Order #{order_number}")
+ |> assign(:order, order)
+ |> assign(:thumbnails, thumbnails)
+
+ {:noreply, socket}
else
- {:noreply, push_navigate(socket, to: ~p"/orders")}
+ {:noreply, push_navigate(socket, to: "/orders")}
end
end
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
+ def handle_event(_event, _params, _socket), do: :cont
end
diff --git a/lib/berrypod_web/live/shop/orders.ex b/lib/berrypod_web/live/shop/pages/orders.ex
similarity index 53%
rename from lib/berrypod_web/live/shop/orders.ex
rename to lib/berrypod_web/live/shop/pages/orders.ex
index 1ceafd0..b3c3852 100644
--- a/lib/berrypod_web/live/shop/orders.ex
+++ b/lib/berrypod_web/live/shop/pages/orders.ex
@@ -1,10 +1,13 @@
-defmodule BerrypodWeb.Shop.Orders do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.Orders do
+ @moduledoc """
+ Orders list page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 3]
alias Berrypod.{Orders, Pages}
- @impl true
- def mount(_params, session, socket) do
+ def init(socket, _params, _uri, session) do
email = session["order_lookup_email"]
page = Pages.get_page("orders")
@@ -21,16 +24,12 @@ defmodule BerrypodWeb.Shop.Orders do
assign(socket, :orders, nil)
end
- {:ok, socket}
+ {:noreply, socket}
end
- @impl true
- def handle_params(_params, _uri, socket), do: {:noreply, socket}
-
- @impl true
- def render(assigns) do
- ~H"""
-
- """
+ def handle_params(_params, _uri, socket) do
+ {:noreply, socket}
end
+
+ def handle_event(_event, _params, _socket), do: :cont
end
diff --git a/lib/berrypod_web/live/shop/product_show.ex b/lib/berrypod_web/live/shop/pages/product.ex
similarity index 92%
rename from lib/berrypod_web/live/shop/product_show.ex
rename to lib/berrypod_web/live/shop/pages/product.ex
index b2ab828..3d7d621 100644
--- a/lib/berrypod_web/live/shop/product_show.ex
+++ b/lib/berrypod_web/live/shop/pages/product.ex
@@ -1,16 +1,20 @@
-defmodule BerrypodWeb.Shop.ProductShow do
- use BerrypodWeb, :live_view
+defmodule BerrypodWeb.Shop.Pages.Product do
+ @moduledoc """
+ Product detail page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 2, assign: 3]
+ import Phoenix.LiveView, only: [connected?: 1, push_navigate: 2]
alias Berrypod.{Analytics, Cart, Pages}
alias Berrypod.Images.Optimizer
alias Berrypod.Products
alias Berrypod.Products.{Product, ProductImage}
- @impl true
- def mount(%{"id" => slug}, _session, socket) do
+ def init(socket, %{"id" => slug}, _uri) do
case Products.get_visible_product(slug) do
nil ->
- {:ok, push_navigate(socket, to: ~p"/collections/all")}
+ {:noreply, push_navigate(socket, to: "/collections/all")}
product ->
all_images =
@@ -61,11 +65,10 @@ defmodule BerrypodWeb.Shop.ProductShow do
# Block data loaders (related_products, reviews) run after product is assigned
extra = Pages.load_block_data(page.blocks, socket.assigns)
- {:ok, assign(socket, extra)}
+ {:noreply, assign(socket, extra)}
end
end
- @impl true
def handle_params(params, _uri, socket) do
if socket.assigns[:product] do
{:noreply, apply_variant_params(params, socket)}
@@ -74,6 +77,50 @@ defmodule BerrypodWeb.Shop.ProductShow do
end
end
+ def handle_event("increment_quantity", _params, socket) do
+ quantity = min(socket.assigns.quantity + 1, 99)
+ {:noreply, assign(socket, :quantity, quantity)}
+ end
+
+ def handle_event("decrement_quantity", _params, socket) do
+ quantity = max(socket.assigns.quantity - 1, 1)
+ {:noreply, assign(socket, :quantity, quantity)}
+ end
+
+ def handle_event("add_to_cart", _params, socket) do
+ variant = socket.assigns.selected_variant
+
+ if variant do
+ cart = Cart.add_item(socket.assigns.raw_cart, variant.id, socket.assigns.quantity)
+
+ if socket.assigns[:analytics_visitor_hash] do
+ Analytics.track_event(
+ "add_to_cart",
+ Map.put(
+ BerrypodWeb.AnalyticsHook.attrs(socket),
+ :pathname,
+ "/products/#{socket.assigns.product.slug}"
+ )
+ )
+ end
+
+ socket =
+ socket
+ |> BerrypodWeb.CartHook.broadcast_and_update(cart)
+ |> assign(:quantity, 1)
+ |> assign(:cart_drawer_open, true)
+ |> assign(:cart_status, "#{socket.assigns.product.title} added to cart")
+
+ {:noreply, socket}
+ else
+ {:noreply, socket}
+ end
+ end
+
+ def handle_event(_event, _params, _socket), do: :cont
+
+ # ── Variant selection logic ──────────────────────────────────────────
+
defp apply_variant_params(params, socket) do
%{option_types: option_types, variants: variants, product: product, all_images: all_images} =
socket.assigns
@@ -149,7 +196,7 @@ defmodule BerrypodWeb.Shop.ProductShow do
opt_type.values
|> Enum.map(fn value ->
params = Map.put(selected_options, opt_type.name, value.title)
- {value.title, ~p"/products/#{slug}?#{params}"}
+ {value.title, "/products/#{slug}?#{URI.encode_query(params)}"}
end)
|> Map.new()
@@ -221,55 +268,7 @@ defmodule BerrypodWeb.Shop.ProductShow do
|> Enum.map(& &1.url)
end
- @impl true
- def handle_event("increment_quantity", _params, socket) do
- quantity = min(socket.assigns.quantity + 1, 99)
- {:noreply, assign(socket, :quantity, quantity)}
- end
-
- @impl true
- def handle_event("decrement_quantity", _params, socket) do
- quantity = max(socket.assigns.quantity - 1, 1)
- {:noreply, assign(socket, :quantity, quantity)}
- end
-
- @impl true
- def handle_event("add_to_cart", _params, socket) do
- variant = socket.assigns.selected_variant
-
- if variant do
- cart = Cart.add_item(socket.assigns.raw_cart, variant.id, socket.assigns.quantity)
-
- if socket.assigns[:analytics_visitor_hash] do
- Analytics.track_event(
- "add_to_cart",
- Map.put(
- BerrypodWeb.AnalyticsHook.attrs(socket),
- :pathname,
- "/products/#{socket.assigns.product.slug}"
- )
- )
- end
-
- socket =
- socket
- |> BerrypodWeb.CartHook.broadcast_and_update(cart)
- |> assign(:quantity, 1)
- |> assign(:cart_drawer_open, true)
- |> assign(:cart_status, "#{socket.assigns.product.title} added to cart")
-
- {:noreply, socket}
- else
- {:noreply, socket}
- end
- end
-
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
+ # ── JSON-LD and meta helpers ─────────────────────────────────────────
defp product_json_ld(product, url, image, base) do
category_slug =
diff --git a/lib/berrypod_web/live/shop/pages/search.ex b/lib/berrypod_web/live/shop/pages/search.ex
new file mode 100644
index 0000000..c84dc47
--- /dev/null
+++ b/lib/berrypod_web/live/shop/pages/search.ex
@@ -0,0 +1,39 @@
+defmodule BerrypodWeb.Shop.Pages.Search do
+ @moduledoc """
+ Search page handler for the unified Shop.Page LiveView.
+ """
+
+ import Phoenix.Component, only: [assign: 3]
+ import Phoenix.LiveView, only: [push_patch: 2]
+
+ alias Berrypod.{Pages, Search}
+
+ def init(socket, _params, _uri) do
+ page = Pages.get_page("search")
+
+ socket =
+ socket
+ |> assign(:page_title, "Search")
+ |> assign(:page, page)
+
+ {:noreply, socket}
+ end
+
+ def handle_params(params, _uri, socket) do
+ query = params["q"] || ""
+ results = if query != "", do: Search.search(query), else: []
+
+ socket =
+ socket
+ |> assign(:search_page_query, query)
+ |> assign(:search_page_results, results)
+
+ {:noreply, socket}
+ end
+
+ def handle_event("search_submit", %{"q" => query}, socket) do
+ {:noreply, push_patch(socket, to: "/search?q=#{query}")}
+ end
+
+ def handle_event(_event, _params, _socket), do: :cont
+end
diff --git a/lib/berrypod_web/live/shop/search.ex b/lib/berrypod_web/live/shop/search.ex
deleted file mode 100644
index 3d20534..0000000
--- a/lib/berrypod_web/live/shop/search.ex
+++ /dev/null
@@ -1,34 +0,0 @@
-defmodule BerrypodWeb.Shop.Search do
- use BerrypodWeb, :live_view
-
- alias Berrypod.{Pages, Search}
-
- @impl true
- def mount(_params, _session, socket) do
- page = Pages.get_page("search")
- {:ok, socket |> assign(:page_title, "Search") |> assign(:page, page)}
- end
-
- @impl true
- def handle_params(params, _uri, socket) do
- query = params["q"] || ""
- results = if query != "", do: Search.search(query), else: []
-
- {:noreply,
- socket
- |> assign(:search_page_query, query)
- |> assign(:search_page_results, results)}
- end
-
- @impl true
- def handle_event("search_submit", %{"q" => query}, socket) do
- {:noreply, push_patch(socket, to: ~p"/search?q=#{query}")}
- end
-
- @impl true
- def render(assigns) do
- ~H"""
-
- """
- end
-end
diff --git a/lib/berrypod_web/page_editor_hook.ex b/lib/berrypod_web/page_editor_hook.ex
index d837478..ab37712 100644
--- a/lib/berrypod_web/page_editor_hook.ex
+++ b/lib/berrypod_web/page_editor_hook.ex
@@ -31,6 +31,7 @@ defmodule BerrypodWeb.PageEditorHook do
# Page editing state
|> assign(:editing, false)
|> assign(:editing_blocks, nil)
+ |> assign(:editor_page_slug, nil)
|> assign(:editor_dirty, false)
|> assign(:editor_at_defaults, true)
|> assign(:editor_history, [])
@@ -67,13 +68,81 @@ defmodule BerrypodWeb.PageEditorHook do
{:cont, socket}
end
- # ── handle_params: track current path ────────────────────────────
+ # ── handle_params: track current path and restore editor state ────
- defp handle_editor_params(_params, uri, socket) do
+ defp handle_editor_params(params, uri, socket) do
parsed = URI.parse(uri)
- # Store the current path for reference (e.g. the Done button)
- {:cont, assign(socket, :editor_current_path, parsed.path)}
+ socket =
+ socket
+ |> assign(:editor_current_path, parsed.path)
+ |> maybe_restore_editor_state(params)
+
+ {:cont, socket}
+ end
+
+ # Restore editor state from URL params on navigation
+ # Only activates state if not already in the requested state (avoids loops)
+ defp maybe_restore_editor_state(socket, params) do
+ if socket.assigns.is_admin do
+ requested_tab = params["edit"]
+ current_tab = socket.assigns.editor_active_tab
+ current_state = socket.assigns.editor_sheet_state
+
+ # If already in the correct state, don't re-apply
+ already_correct? =
+ current_state == :open && requested_tab &&
+ String.to_existing_atom(requested_tab) == current_tab
+
+ if already_correct? do
+ socket
+ else
+ case requested_tab do
+ "theme" ->
+ socket
+ |> assign(:editor_sheet_state, :open)
+ |> assign(:editor_active_tab, :theme)
+ |> maybe_enter_theme_mode()
+
+ "page" ->
+ socket
+ |> assign(:editor_sheet_state, :open)
+ |> assign(:editor_active_tab, :page)
+ |> maybe_enter_page_mode()
+
+ "settings" ->
+ socket
+ |> assign(:editor_sheet_state, :open)
+ |> assign(:editor_active_tab, :settings)
+ |> maybe_enter_theme_mode()
+
+ _ ->
+ socket
+ end
+ end
+ else
+ socket
+ end
+ end
+
+ defp maybe_enter_theme_mode(socket) do
+ if socket.assigns.theme_editing do
+ socket
+ else
+ load_theme_state(socket)
+ end
+ end
+
+ defp maybe_enter_page_mode(socket) do
+ if socket.assigns.editing do
+ socket
+ else
+ if socket.assigns[:page] do
+ enter_edit_mode(socket)
+ else
+ socket
+ end
+ end
end
# ── handle_info ─────────────────────────────────────────────────
@@ -703,6 +772,7 @@ defmodule BerrypodWeb.PageEditorHook do
socket
|> assign(:editing, true)
|> assign(:editing_blocks, page.blocks)
+ |> assign(:editor_page_slug, page.slug)
|> assign(:editor_dirty, false)
|> assign(:editor_at_defaults, at_defaults)
|> assign(:editor_history, [])
@@ -725,6 +795,7 @@ defmodule BerrypodWeb.PageEditorHook do
socket
|> assign(:editing, false)
|> assign(:editing_blocks, nil)
+ |> assign(:editor_page_slug, nil)
|> assign(:editor_dirty, false)
|> assign(:editor_history, [])
|> assign(:editor_future, [])
diff --git a/lib/berrypod_web/page_renderer.ex b/lib/berrypod_web/page_renderer.ex
index 96e56a2..7d19c6d 100644
--- a/lib/berrypod_web/page_renderer.ex
+++ b/lib/berrypod_web/page_renderer.ex
@@ -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"""
-
- <%= if @theme_editor_settings do %>
- <%!-- Shop name --%>
-
- Shop name
-
-
-
- <%!-- Presets --%>
-
-
Preset
-
- <%= for {preset_name, description} <- @theme_editor_presets do %>
-
- {preset_name}
- {description}
-
- <% end %>
-
-
-
- <%!-- Mood --%>
-
-
Colour mood
-
- <%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
-
- {mood}
-
- <% end %>
-
-
-
- <%!-- Typography --%>
-
-
Font style
-
- <%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
-
- {typo}
-
- <% end %>
-
-
-
- <%!-- Shape --%>
-
-
Corner style
-
- <%= for shape <- ["sharp", "soft", "round", "pill"] do %>
-
- {shape}
-
- <% end %>
-
-
-
- <%!-- More options link --%>
-
-
- More options
-
-
-
-
-
-
- For full theme customisation including branding, colours, and layout, visit the theme editor .
-
-
-
- <% else %>
-
Loading theme settings...
- <% end %>
-
+ <.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
<.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
<.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 %>
<.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 %>
No products found in this collection.
- <.link navigate={~p"/collections/all"} class="collection-empty-link">
+ <.link patch={~p"/collections/all"} class="collection-empty-link">
View all products
@@ -1020,7 +903,7 @@ defmodule BerrypodWeb.PageRenderer do
Please wait while we confirm your payment. This usually takes a few seconds.
- If this page doesn't update, please <.link navigate="/contact" class="checkout-contact-link">contact us.
+ If this page doesn't update, please <.link patch="/contact" class="checkout-contact-link">contact us.
<% end %>
@@ -1045,20 +928,20 @@ defmodule BerrypodWeb.PageRenderer do