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

@@ -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

View 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

View File

@@ -0,0 +1,105 @@
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}
@sort_options [
{"featured", "Featured"},
{"newest", "Newest"},
{"price_asc", "Price: Low to High"},
{"price_desc", "Price: High to Low"},
{"name_asc", "Name: A-Z"},
{"name_desc", "Name: Z-A"}
]
def init(socket, _params, _uri) do
page = Pages.get_page("collection")
socket =
socket
|> assign(:page, page)
|> assign(:sort_options, @sort_options)
|> assign(:current_sort, "featured")
{:noreply, socket}
end
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} ->
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 ->
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}
end
defp load_collection("sale", sort, page) do
pagination = Products.list_visible_products_paginated(on_sale: true, sort: sort, page: page)
{:ok, "Sale", :sale, pagination}
end
defp load_collection(slug, sort, page) do
case Enum.find(Products.list_categories(), &(&1.slug == slug)) do
nil ->
:not_found
category ->
pagination =
Products.list_visible_products_paginated(
category: category.name,
sort: sort,
page: page
)
{:ok, category.name, category, pagination}
end
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."
end

View File

@@ -0,0 +1,72 @@
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
def init(socket, _params, _uri) do
page = Pages.get_page("contact")
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
def handle_event("lookup_orders", %{"email" => email}, socket) do
orders = Orders.list_orders_by_email(email)
state =
if orders == [] do
:not_found
else
token = OrderLookupController.generate_token(email)
link = BerrypodWeb.Endpoint.url() <> "/orders/verify/#{token}"
OrderNotifier.deliver_order_lookup(email, link)
:sent
end
{:noreply, assign(socket, :tracking_state, state)}
end
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: "/contact")}
{:error, :invalid_params} ->
{:noreply, put_flash(socket, :error, "Please fill in all required fields.")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Sorry, something went wrong. Please try again.")}
end
end
def handle_event("reset_tracking", _params, socket) do
{:noreply, assign(socket, :tracking_state, :idle)}
end
def handle_event(_event, _params, _socket), do: :cont
end

View File

@@ -0,0 +1,79 @@
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
def init(socket, _params, _uri) do
# Content pages load in handle_params based on live_action
{:noreply, socket}
end
def handle_params(_params, _uri, socket) do
action = socket.assigns.live_action
slug = to_string(action)
page = Pages.get_page(slug)
{seo, content_blocks} = page_config(action)
socket =
socket
|> assign(seo)
|> assign(:page, page)
|> assign(:content_blocks, content_blocks)
{:noreply, socket}
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 this is sample content for the demo shop",
og_url: BerrypodWeb.Endpoint.url() <> "/about"
},
PreviewData.about_content()
}
end
defp page_config(:delivery) do
{
%{
page_title: "Delivery & returns",
page_description: "Everything you need to know about shipping and returns.",
og_url: BerrypodWeb.Endpoint.url() <> "/delivery"
},
LegalPages.delivery_content()
}
end
defp page_config(:privacy) do
{
%{
page_title: "Privacy policy",
page_description: "How we handle your personal information.",
og_url: BerrypodWeb.Endpoint.url() <> "/privacy"
},
LegalPages.privacy_content()
}
end
defp page_config(:terms) do
{
%{
page_title: "Terms of service",
page_description: "The terms and conditions governing purchases from our shop.",
og_url: BerrypodWeb.Endpoint.url() <> "/terms"
},
LegalPages.terms_content()
}
end
end

View File

@@ -0,0 +1,68 @@
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
def init(socket, _params, _uri) do
# Custom pages load in handle_params based on slug
{:noreply, socket}
end
def handle_params(%{"slug" => slug}, _uri, socket) do
page = Pages.get_page(slug)
cond do
is_nil(page) ->
record_broken_url("/#{slug}")
raise BerrypodWeb.NotFoundError
page.type != "custom" ->
raise BerrypodWeb.NotFoundError
page.published != true and not socket.assigns.is_admin ->
raise BerrypodWeb.NotFoundError
true ->
extra = Pages.load_block_data(page.blocks, socket.assigns)
base = BerrypodWeb.Endpoint.url()
socket =
socket
|> assign(:page_title, page.title)
|> assign(:page, page)
|> maybe_assign_meta(page, base)
|> assign(extra)
{:noreply, socket}
end
end
def handle_event(_event, _params, _socket), do: :cont
defp record_broken_url(path) do
prior_hits = Berrypod.Analytics.count_pageviews_for_path(path)
Berrypod.Redirects.record_broken_url(path, prior_hits)
if prior_hits > 0 do
Berrypod.Redirects.attempt_auto_resolve(path)
end
rescue
_ -> :ok
end
defp maybe_assign_meta(socket, page, base) do
socket
|> assign(:og_url, base <> "/#{page.slug}")
|> then(fn s ->
if page.meta_description do
assign(s, :page_description, page.meta_description)
else
s
end
end)
end
end

View File

@@ -0,0 +1,44 @@
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
def init(socket, _params, _uri) do
page = Pages.get_page("home")
extra = Pages.load_block_data(page.blocks, socket.assigns)
base = BerrypodWeb.Endpoint.url()
site_name = socket.assigns.site_name
org_ld =
Jason.encode!(
%{
"@context" => "https://schema.org",
"@type" => "Organization",
"name" => site_name,
"url" => base <> "/"
},
escape: :html_safe
)
socket =
socket
|> assign(:page_title, "Home")
|> assign(:og_url, base <> "/")
|> assign(:json_ld, org_ld)
|> assign(:page, page)
|> assign(extra)
{:noreply, socket}
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end
def handle_event(_event, _params, _socket), do: :cont
end

View File

@@ -0,0 +1,62 @@
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
def init(socket, _params, _uri, session) do
page = Pages.get_page("order_detail")
socket =
socket
|> assign(:lookup_email, session["order_lookup_email"])
|> assign(:page, page)
{:noreply, socket}
end
def handle_params(%{"order_number" => order_number}, _uri, socket) do
email = socket.assigns.lookup_email
order = Orders.get_order_by_number(order_number)
normalised = fn e -> String.downcase(String.trim(e || "")) end
if order && order.payment_status == "paid" &&
email && normalised.(order.customer_email) == normalised.(email) do
variant_ids = Enum.map(order.items, & &1.variant_id)
variants = Products.get_variants_with_products(variant_ids)
thumbnails =
Map.new(variants, fn {id, variant} ->
thumb =
case variant.product.images do
[first | _] -> ProductImage.thumbnail_url(first)
_ -> nil
end
slug = if variant.product.visible, do: variant.product.slug, else: nil
{id, %{thumb: thumb, slug: slug}}
end)
socket =
socket
|> assign(:page_title, "Order #{order_number}")
|> assign(:order, order)
|> assign(:thumbnails, thumbnails)
{:noreply, socket}
else
{:noreply, push_navigate(socket, to: "/orders")}
end
end
def handle_event(_event, _params, _socket), do: :cont
end

View File

@@ -0,0 +1,35 @@
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}
def init(socket, _params, _uri, session) do
email = session["order_lookup_email"]
page = Pages.get_page("orders")
socket =
socket
|> assign(:page_title, "Your orders")
|> assign(:lookup_email, email)
|> assign(:page, page)
socket =
if email do
assign(socket, :orders, Orders.list_orders_by_email(email))
else
assign(socket, :orders, nil)
end
{:noreply, socket}
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end
def handle_event(_event, _params, _socket), do: :cont
end

View File

@@ -0,0 +1,357 @@
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}
def init(socket, %{"id" => slug}, _uri) do
case Products.get_visible_product(slug) do
nil ->
{:noreply, push_navigate(socket, to: "/collections/all")}
product ->
all_images =
(product.images || [])
|> Enum.sort_by(& &1.position)
|> Enum.map(fn img ->
width =
case ProductImage.source_width(img) do
nil -> 1200
sw -> Enum.max(Optimizer.applicable_widths(sw))
end
%{url: ProductImage.url(img, width), color: img.color}
end)
|> Enum.reject(fn img -> is_nil(img.url) end)
option_types = Product.option_types(product)
variants = product.variants || []
if connected?(socket) and socket.assigns[:analytics_visitor_hash] do
Analytics.track_event(
"product_view",
Map.put(BerrypodWeb.AnalyticsHook.attrs(socket), :pathname, "/products/#{slug}")
)
end
base = BerrypodWeb.Endpoint.url()
og_url = base <> "/products/#{slug}"
og_image = og_image_url(all_images)
page = Pages.get_page("pdp")
socket =
socket
|> assign(:page_title, product.title)
|> assign(:page_description, meta_description(product.description))
|> assign(:og_type, "product")
|> assign(:og_url, og_url)
|> assign(:og_image, og_image)
|> assign(:json_ld, product_json_ld(product, og_url, og_image, base))
|> assign(:product, product)
|> assign(:all_images, all_images)
|> assign(:quantity, 1)
|> assign(:option_types, option_types)
|> assign(:variants, variants)
|> assign(:page, page)
# Block data loaders (related_products, reviews) run after product is assigned
extra = Pages.load_block_data(page.blocks, socket.assigns)
{:noreply, assign(socket, extra)}
end
end
def handle_params(params, _uri, socket) do
if socket.assigns[:product] do
{:noreply, apply_variant_params(params, socket)}
else
{:noreply, socket}
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
option_names = Enum.map(option_types, & &1.name)
default_options =
case variants do
[first | _] -> first.options
[] -> %{}
end
# Extract option params from the URL, ignoring unknown keys
param_options =
Enum.reduce(option_names, %{}, fn name, acc ->
case params[name] do
nil -> acc
value -> Map.put(acc, name, value)
end
end)
{selected_options, changed_option} =
if map_size(param_options) == 0 do
{default_options, nil}
else
merged = Map.merge(default_options, param_options)
# Identify which option was changed from the default
changed =
Enum.find(option_names, fn name ->
Map.has_key?(param_options, name) and param_options[name] != default_options[name]
end)
{merged, changed}
end
selected_options =
if changed_option do
resolve_valid_combo(variants, option_types, selected_options, changed_option)
else
selected_options
end
selected_variant = find_variant(variants, selected_options)
# If params produced an invalid combo with no matching variant, fall back to defaults
{selected_options, selected_variant} =
if is_nil(selected_variant) and variants != [] do
opts = List.first(variants).options
{opts, List.first(variants)}
else
{selected_options, selected_variant}
end
available_options = compute_available_options(option_types, variants, selected_options)
display_price = variant_price(selected_variant, product)
gallery_images = filter_gallery_images(all_images, selected_options["Color"])
option_urls = build_option_urls(option_types, selected_options, product.slug)
socket
|> assign(:selected_options, selected_options)
|> assign(:selected_variant, selected_variant)
|> assign(:available_options, available_options)
|> assign(:display_price, display_price)
|> assign(:gallery_images, gallery_images)
|> assign(:option_urls, option_urls)
end
defp build_option_urls(option_types, selected_options, slug) do
Enum.reduce(option_types, %{}, fn opt_type, acc ->
urls =
opt_type.values
|> Enum.map(fn value ->
params = Map.put(selected_options, opt_type.name, value.title)
{value.title, "/products/#{slug}?#{URI.encode_query(params)}"}
end)
|> Map.new()
Map.put(acc, opt_type.name, urls)
end)
end
defp compute_available_options(option_types, variants, selected_options) do
Enum.reduce(option_types, %{}, fn opt_type, acc ->
other_options = Map.delete(selected_options, opt_type.name)
available_values =
variants
|> Enum.filter(fn v ->
v.is_available &&
Enum.all?(other_options, fn {k, selected_val} ->
v.options[k] == selected_val
end)
end)
|> Enum.map(fn v -> v.options[opt_type.name] end)
|> Enum.uniq()
Map.put(acc, opt_type.name, available_values)
end)
end
defp variant_price(%{price: price}, _product) when is_integer(price), do: price
defp variant_price(_, %{cheapest_price: price}), do: price
defp variant_price(_, _), do: 0
# If the current combo doesn't match any variant, auto-adjust other options
# to find a valid one. Keeps the just-changed option fixed, adjusts the rest.
defp resolve_valid_combo(variants, option_types, selected_options, changed_option) do
if Enum.any?(variants, fn v -> v.options == selected_options end) do
selected_options
else
matching =
Enum.filter(variants, fn v ->
v.is_available && v.options[changed_option] == selected_options[changed_option]
end)
case matching do
[first | _] ->
Enum.reduce(option_types, selected_options, fn opt_type, acc ->
if opt_type.name == changed_option do
acc
else
Map.put(acc, opt_type.name, first.options[opt_type.name])
end
end)
[] ->
selected_options
end
end
end
defp find_variant(variants, selected_options) do
Enum.find(variants, fn v -> v.options == selected_options end)
end
defp filter_gallery_images(all_images, selected_color) do
if selected_color do
color_images = Enum.filter(all_images, &(&1.color == selected_color))
if color_images == [], do: all_images, else: color_images
else
all_images
end
|> Enum.map(& &1.url)
end
# ── JSON-LD and meta helpers ─────────────────────────────────────────
defp product_json_ld(product, url, image, base) do
category_slug =
if product.category,
do: product.category |> String.downcase() |> String.replace(" ", "-"),
else: "all"
breadcrumbs =
[
%{"@type" => "ListItem", "position" => 1, "name" => "Home", "item" => base <> "/"},
product.category &&
%{
"@type" => "ListItem",
"position" => 2,
"name" => product.category,
"item" => base <> "/collections/#{category_slug}"
},
%{
"@type" => "ListItem",
"position" => if(product.category, do: 3, else: 2),
"name" => product.title,
"item" => url
}
]
|> Enum.reject(&is_nil/1)
data = %{
"@context" => "https://schema.org",
"@graph" => [
%{
"@type" => "Product",
"name" => product.title,
"description" => plain_text(product.description),
"image" => Enum.reject([image], &is_nil/1),
"url" => url,
"offers" => %{
"@type" => "Offer",
"price" => format_price(product.cheapest_price),
"priceCurrency" => "GBP",
"availability" =>
if(product.in_stock,
do: "https://schema.org/InStock",
else: "https://schema.org/OutOfStock"
),
"url" => url
}
},
%{
"@type" => "BreadcrumbList",
"itemListElement" => breadcrumbs
}
]
}
Jason.encode!(data, escape: :html_safe)
end
defp format_price(pence) when is_integer(pence) do
"#{div(pence, 100)}.#{String.pad_leading(to_string(rem(pence, 100)), 2, "0")}"
end
defp format_price(_), do: "0.00"
defp plain_text(nil), do: nil
defp plain_text(text), do: String.replace(text, ~r/<[^>]+>/, "")
defp og_image_url([%{url: "/" <> _ = path} | _]), do: BerrypodWeb.Endpoint.url() <> path
defp og_image_url(_), do: nil
defp meta_description(nil), do: nil
defp meta_description(text) do
plain = String.replace(text, ~r/<[^>]+>/, "")
if String.length(plain) <= 155 do
plain
else
plain
|> String.slice(0, 155)
|> String.split(~r/\s+/)
|> Enum.drop(-1)
|> Enum.join(" ")
|> Kernel.<>("")
end
end
end

View 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