berrypod/lib/berrypod_web/live/shop/page.ex
jamey 89c411e0fc
All checks were successful
deploy / deploy (push) Successful in 1m27s
clear page-specific assigns on navigation to prevent stale data
When navigating between page types in the unified shop LiveView,
assigns from the previous page could persist and cause stale data
to appear or template errors. Now explicitly nils them out.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-09 18:34:45 +00:00

215 lines
6.0 KiB
Elixir

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 - clear previous page's assigns and call init
socket =
socket
|> assign(:_current_page_action, action)
|> clear_page_specific_assigns(prev_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"""
<BerrypodWeb.PageRenderer.render_page {assigns} />
"""
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
# Clear assigns that are specific to each page type to prevent stale data
# from bleeding into the next page's render
defp clear_page_specific_assigns(socket, :product) do
socket
|> assign(:product, nil)
|> assign(:all_images, nil)
|> assign(:variants, nil)
|> assign(:option_types, nil)
|> assign(:selected_variant, nil)
|> assign(:selected_options, nil)
|> assign(:available_options, nil)
|> assign(:display_price, nil)
|> assign(:gallery_images, nil)
|> assign(:option_urls, nil)
|> assign(:related_products, nil)
|> assign(:json_ld, nil)
end
defp clear_page_specific_assigns(socket, :collection) do
socket
|> assign(:products, nil)
|> assign(:pagination, nil)
|> assign(:current_sort, nil)
|> assign(:collection_title, nil)
|> assign(:collection_slug, nil)
end
defp clear_page_specific_assigns(socket, :contact) do
socket
|> assign(:contact_form, nil)
|> assign(:lookup_form, nil)
|> assign(:looked_up_orders, nil)
end
defp clear_page_specific_assigns(socket, :search) do
assign(socket, :search_results, nil)
end
defp clear_page_specific_assigns(socket, :orders) do
assign(socket, :orders, nil)
end
defp clear_page_specific_assigns(socket, :order_detail) do
socket
|> assign(:order, nil)
|> assign(:email_form, nil)
end
defp clear_page_specific_assigns(socket, :checkout_success) do
socket
|> assign(:order, nil)
|> assign(:js_purchase_tracked, nil)
|> assign(:css_purchase_tracked, nil)
end
defp clear_page_specific_assigns(socket, _), do: socket
end