consolidate shop pages into unified LiveView for editor state persistence
All checks were successful
deploy / deploy (push) Successful in 1m27s
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:
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -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"""
|
||||
|
||||
154
lib/berrypod_web/live/shop/page.ex
Normal file
154
lib/berrypod_web/live/shop/page.ex
Normal file
@@ -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"""
|
||||
<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
|
||||
end
|
||||
31
lib/berrypod_web/live/shop/pages/cart.ex
Normal file
31
lib/berrypod_web/live/shop/pages/cart.ex
Normal file
@@ -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
|
||||
82
lib/berrypod_web/live/shop/pages/checkout_success.ex
Normal file
82
lib/berrypod_web/live/shop/pages/checkout_success.ex
Normal file
@@ -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
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
def handle_event(_event, _params, _socket), do: :cont
|
||||
end
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
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()
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
def handle_event(_event, _params, _socket), do: :cont
|
||||
|
||||
defp record_broken_url(path) do
|
||||
prior_hits = Berrypod.Analytics.count_pageviews_for_path(path)
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
def handle_params(_params, _uri, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event(_event, _params, _socket), do: :cont
|
||||
end
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
def handle_event(_event, _params, _socket), do: :cont
|
||||
end
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
def handle_params(_params, _uri, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event(_event, _params, _socket), do: :cont
|
||||
end
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
# ── JSON-LD and meta helpers ─────────────────────────────────────────
|
||||
|
||||
defp product_json_ld(product, url, image, base) do
|
||||
category_slug =
|
||||
39
lib/berrypod_web/live/shop/pages/search.ex
Normal file
39
lib/berrypod_web/live/shop/pages/search.ex
Normal file
@@ -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
|
||||
@@ -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"""
|
||||
<BerrypodWeb.PageRenderer.render_page {assigns} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user