defmodule BerrypodWeb.PageRenderer do @moduledoc """ Generic page renderer for the page builder. One `render_page/1` renders ALL pages — no page-specific render functions. Each page is a flat list of blocks rendered in order. Block components dispatch to existing shop components. """ use Phoenix.Component use BerrypodWeb.ShopComponents use Phoenix.VerifiedRoutes, endpoint: BerrypodWeb.Endpoint, router: BerrypodWeb.Router, statics: BerrypodWeb.static_paths() import BerrypodWeb.BlockEditorComponents import BerrypodWeb.CoreComponents, only: [icon: 1, external_link: 1] alias Berrypod.Cart # ── Public API ────────────────────────────────────────────────── @doc """ Renders a full page from its block definition. Expects `@page` (with `:slug` and `:blocks`) plus all the standard layout assigns (theme_settings, cart_items, etc.). When `@editing` is true and `@editing_blocks` is set (admin using the live page editor), wraps the page in a sidebar + content layout. """ def render_page(assigns) do assigns = assign(assigns, :block_assigns, block_assigns(assigns)) if assigns[:is_admin] do render_page_with_rail(assigns) else render_page_normal(assigns) end end defp render_page_normal(assigns) do ~H""" <.shop_layout {layout_assigns(assigns)} active_page={@page.slug} error_page={@page.slug == "error"} >
{render_block(Map.merge(@block_assigns, %{block: block, page_slug: @page.slug}))}
""" end defp render_page_with_rail(assigns) do ~H""" <.shop_layout {layout_assigns(assigns)} active_page={@page.slug} error_page={@page.slug == "error"} >
<%= if @editing && @editing_blocks do %>
{render_block(Map.merge(@block_assigns, %{block: block, page_slug: @page.slug}))}
<% else %>
{render_block(Map.merge(@block_assigns, %{block: block, page_slug: @page.slug}))}
<% end %>
<%!-- Editor sheet for page/theme/settings editing --%> <.editor_sheet editing={@editing} theme_editing={Map.get(assigns, :theme_editing, false)} editor_dirty={@editor_dirty} theme_dirty={Map.get(assigns, :theme_dirty, false)} site_dirty={Map.get(assigns, :site_dirty, false)} editor_sheet_state={assigns[:editor_sheet_state] || :collapsed} editor_save_status={@editor_save_status} editor_active_tab={Map.get(assigns, :editor_active_tab, :page)} editor_nav_blocked={Map.get(assigns, :editor_nav_blocked)} has_editable_page={@page != nil} > <.editor_panel_content editor_active_tab={Map.get(assigns, :editor_active_tab, :page)} page={@page} editing_blocks={@editing_blocks} editor_history={@editor_history} editor_future={@editor_future} editor_dirty={@editor_dirty} editor_live_region_message={@editor_live_region_message} editor_expanded={@editor_expanded} editor_show_picker={@editor_show_picker} editor_picker_filter={@editor_picker_filter} editor_allowed_blocks={@editor_allowed_blocks} editor_image_picker_block_id={@editor_image_picker_block_id} editor_image_picker_images={@editor_image_picker_images} editor_image_picker_search={@editor_image_picker_search} editor_at_defaults={Map.get(assigns, :editor_at_defaults, true)} theme_editor_settings={Map.get(assigns, :theme_editor_settings)} theme_editor_active_preset={Map.get(assigns, :theme_editor_active_preset)} theme_editor_presets={Map.get(assigns, :theme_editor_presets, [])} theme_editor_customise_open={Map.get(assigns, :theme_editor_customise_open, false)} theme_editor_logo_image={Map.get(assigns, :theme_editor_logo_image)} theme_editor_header_image={Map.get(assigns, :theme_editor_header_image)} theme_editor_icon_image={Map.get(assigns, :theme_editor_icon_image)} theme_editor_contrast_warning={Map.get(assigns, :theme_editor_contrast_warning, :ok)} uploads={Map.get(assigns, :uploads)} site_name={Map.get(assigns, :site_name, "")} settings_form={Map.get(assigns, :settings_form)} settings_dirty={Map.get(assigns, :settings_dirty, false)} settings_save_status={Map.get(assigns, :settings_save_status, :idle)} site_state={Map.get(assigns, :site_state)} site_nav_pages={Map.get(assigns, :site_nav_pages, [])} /> """ end # Editor panel content dispatcher - shows content based on active tab attr :editor_active_tab, :atom, default: :page attr :page, :map, default: nil attr :editing_blocks, :list, default: nil attr :editor_history, :list, default: [] attr :editor_future, :list, default: [] attr :editor_dirty, :boolean, default: false attr :editor_live_region_message, :string, default: nil attr :editor_expanded, :any, default: nil attr :editor_show_picker, :boolean, default: false attr :editor_picker_filter, :string, default: "" attr :editor_allowed_blocks, :list, default: nil attr :editor_image_picker_block_id, :string, default: nil attr :editor_image_picker_images, :list, default: [] attr :editor_image_picker_search, :string, default: "" attr :editor_at_defaults, :boolean, default: true attr :theme_editor_settings, :map, default: nil attr :theme_editor_active_preset, :atom, default: nil attr :theme_editor_presets, :list, default: [] attr :theme_editor_customise_open, :boolean, default: false attr :theme_editor_logo_image, :map, default: nil attr :theme_editor_header_image, :map, default: nil attr :theme_editor_icon_image, :map, default: nil attr :theme_editor_contrast_warning, :atom, default: :ok attr :uploads, :map, default: nil attr :site_name, :string, default: "" attr :settings_form, :map, default: nil attr :settings_dirty, :boolean, default: false attr :settings_save_status, :atom, default: :idle attr :site_state, :any, default: nil attr :site_nav_pages, :list, default: [] defp editor_panel_content(%{editor_active_tab: :page} = assigns) do ~H""" <.editor_sheet_content page={@page} editing_blocks={@editing_blocks} editor_history={@editor_history} editor_future={@editor_future} editor_dirty={@editor_dirty} editor_live_region_message={@editor_live_region_message} editor_expanded={@editor_expanded} editor_show_picker={@editor_show_picker} editor_picker_filter={@editor_picker_filter} editor_allowed_blocks={@editor_allowed_blocks} editor_image_picker_block_id={@editor_image_picker_block_id} editor_image_picker_images={@editor_image_picker_images} editor_image_picker_search={@editor_image_picker_search} editor_at_defaults={@editor_at_defaults} settings_form={@settings_form} settings_dirty={@settings_dirty} settings_save_status={@settings_save_status} /> """ end defp editor_panel_content(%{editor_active_tab: :theme} = assigns) do ~H""" <.theme_editor_content theme_editor_settings={@theme_editor_settings} theme_editor_active_preset={@theme_editor_active_preset} theme_editor_presets={@theme_editor_presets} theme_editor_customise_open={@theme_editor_customise_open} theme_editor_logo_image={@theme_editor_logo_image} theme_editor_header_image={@theme_editor_header_image} theme_editor_icon_image={@theme_editor_icon_image} theme_editor_contrast_warning={@theme_editor_contrast_warning} uploads={@uploads} site_name={@site_name} /> """ end defp editor_panel_content(%{editor_active_tab: :site} = assigns) do ~H""" """ end # 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: [] attr :theme_editor_customise_open, :boolean, default: false attr :theme_editor_logo_image, :map, default: nil attr :theme_editor_header_image, :map, default: nil attr :theme_editor_icon_image, :map, default: nil attr :theme_editor_contrast_warning, :atom, default: :ok attr :uploads, :map, default: nil attr :site_name, :string, default: "" defp theme_editor_content(assigns) do ~H""" <.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} uploads={@uploads} logo_image={@theme_editor_logo_image} header_image={@theme_editor_header_image} icon_image={@theme_editor_icon_image} contrast_warning={@theme_editor_contrast_warning} event_prefix="theme_" /> """ end # Editor sheet content - the block list and editing controls attr :page, :map, default: nil attr :editing_blocks, :list, default: nil attr :editor_history, :list, default: [] attr :editor_future, :list, default: [] attr :editor_dirty, :boolean, default: false attr :editor_live_region_message, :string, default: nil attr :editor_expanded, :any, default: nil attr :editor_show_picker, :boolean, default: false attr :editor_picker_filter, :string, default: "" attr :editor_allowed_blocks, :list, default: nil attr :editor_image_picker_block_id, :string, default: nil attr :editor_image_picker_images, :list, default: [] attr :editor_image_picker_search, :string, default: "" attr :editor_at_defaults, :boolean, default: true attr :settings_form, :map, default: nil attr :settings_dirty, :boolean, default: false attr :settings_save_status, :atom, default: :idle defp editor_sheet_content(assigns) do ~H"""
<%!-- Page title and undo/redo --%>

{@page.title}

<%!-- ARIA live region for screen reader announcements --%>
{if @editor_live_region_message, do: @editor_live_region_message}
<%!-- Page settings (custom and system pages, not product/collection) --%> <.page_settings_section :if={@page[:type] in ["custom", "system"]} page={@page} form={@settings_form} dirty={@settings_dirty} save_status={@settings_save_status} /> <%!-- Block list --%>
<.block_card :for={{block, idx} <- Enum.with_index(@editing_blocks || [])} block={block} idx={idx} total={length(@editing_blocks || [])} expanded={@editor_expanded || MapSet.new()} event_prefix="editor_" />

No blocks on this page yet.

<%!-- Add block button --%>
<%!-- Block picker modal --%> <.block_picker :if={@editor_show_picker} allowed_blocks={@editor_allowed_blocks} filter={@editor_picker_filter} event_prefix="editor_" /> <%!-- Image picker modal --%> <.image_picker :if={@editor_image_picker_block_id} images={@editor_image_picker_images} search={@editor_image_picker_search} event_prefix="editor_" />
""" end # Page settings section (collapsible) for custom and system pages attr :page, :map, required: true attr :form, :map, default: nil attr :dirty, :boolean, default: false attr :save_status, :atom, default: :idle defp page_settings_section(assigns) do form = assigns.form || %{} is_custom = assigns.page[:type] == "custom" page = assigns.page assigns = assigns |> assign(:is_custom, is_custom) |> assign(:form_title, form["title"] || page[:title] || "") |> assign(:form_slug, form["slug"] || page[:slug] || "") |> assign(:form_meta, form["meta_description"] || page[:meta_description] || "") |> assign(:form_published, form_checked?(form, "published", page[:published])) |> assign(:form_show_in_nav, form_checked?(form, "show_in_nav", page[:show_in_nav])) |> assign(:form_nav_label, form["nav_label"] || page[:nav_label] || "") |> assign( :form_nav_position, form["nav_position"] || to_string(page[:nav_position] || 0) ) ~H"""
<.icon name="hero-cog-6-tooth-mini" class="size-4" /> Page settings <.icon name="hero-chevron-down-mini" class="size-4 page-settings-chevron" />
/
<%!-- Published and nav options only for custom pages --%>
""" end defp form_checked?(form, key, page_value) when is_map(form) do case form[key] do "true" -> true "false" -> false nil -> page_value == true _ -> page_value == true end end # ── Block dispatch ────────────────────────────────────────────── defp render_block(%{block: %{"type" => "hero"}} = assigns) do settings = assigns.block["settings"] || %{} variant = hero_variant(settings["variant"]) # Page-context assigns (e.g. error_title, error_code) override block settings assigns = assigns |> assign(:title, assigns[:error_title] || settings["title"] || "") |> assign(:description, assigns[:error_description] || settings["description"] || "") |> assign(:variant, variant) |> assign(:background, hero_background(settings["variant"])) |> assign(:pre_title, assigns[:error_code] || settings["pre_title"]) |> assign(:cta_text, settings["cta_text"]) |> assign(:cta_href, settings["cta_href"]) |> assign(:secondary_cta_text, settings["secondary_cta_text"]) |> assign(:secondary_cta_href, settings["secondary_cta_href"]) ~H""" <.hero_section title={@title} description={@description} variant={@variant} background={@background} pre_title={@pre_title} cta_text={@cta_text} cta_href={@cta_href} secondary_cta_text={@secondary_cta_text} secondary_cta_href={@secondary_cta_href} mode={@mode} /> """ end defp render_block(%{block: %{"type" => "category_nav"}} = assigns) do ~H""" <.category_nav categories={assigns[:categories] || []} mode={@mode} /> """ end defp render_block(%{block: %{"type" => "featured_products"}} = assigns) do settings = assigns.block["settings"] || %{} assigns = assigns |> assign(:section_title, settings["title"] || "Featured products") |> assign(:layout, settings["layout"] || "section") |> assign(:card_variant, card_variant(settings["card_variant"])) |> assign(:columns, grid_columns(settings["columns"])) ~H""" <%= if @layout == "grid" do %> <.product_grid columns={@columns} theme_settings={@theme_settings}> <%= for product <- assigns[:products] || [] do %> <.product_card product={product} theme_settings={@theme_settings} mode={@mode} variant={@card_variant} /> <% end %> <% else %> <.featured_products_section title={@section_title} products={assigns[:products] || []} theme_settings={@theme_settings} mode={@mode} /> <% end %> """ end defp render_block(%{block: %{"type" => "image_text"}} = assigns) do settings = assigns.block["settings"] || %{} image_url = resolve_block_image_url(settings["image_id"]) assigns = assigns |> assign(:section_title, settings["title"] || "") |> assign(:section_description, settings["description"] || "") |> assign(:image_url, image_url) |> assign(:link_text, settings["link_text"]) |> assign(:link_href, settings["link_href"]) ~H""" <.image_text_section title={@section_title} description={@section_description} image_url={@image_url} link_text={@link_text} link_href={@link_href} mode={@mode} /> """ end defp render_block(%{block: %{"type" => "newsletter_card"}} = assigns) do settings = assigns.block["settings"] || %{} assigns = assigns |> assign(:title, settings["title"] || "Newsletter") |> assign(:description, settings["description"] || "") |> assign(:button_text, settings["button_text"] || "Subscribe") |> assign(:newsletter_state, assigns[:newsletter_state] || :idle) |> assign(:newsletter_enabled, assigns[:newsletter_enabled] || false) ~H""" <.newsletter_card title={@title} description={@description} button_text={@button_text} newsletter_state={@newsletter_state} newsletter_enabled={@newsletter_enabled} /> """ end defp render_block(%{block: %{"type" => "social_links_card"}} = assigns) do ~H"<.social_links_card links={assigns[:social_links] || []} />" end defp render_block(%{block: %{"type" => "info_card"}} = assigns) do settings = assigns.block["settings"] || %{} items = (settings["items"] || []) |> Enum.map(fn item -> %{label: item["label"] || item[:label], value: item["value"] || item[:value]} end) assigns = assigns |> assign(:card_title, settings["title"] || "") |> assign(:items, items) ~H""" <.info_card title={@card_title} items={@items} /> """ end defp render_block(%{block: %{"type" => "trust_badges"}} = assigns) do ~H""" <.trust_badges :if={@theme_settings.pdp_trust_badges} /> """ end defp render_block(%{block: %{"type" => "reviews_section"}} = assigns) do ~H""" <.reviews_section :if={@theme_settings.pdp_reviews} reviews={assigns[:reviews] || []} average_rating={assigns[:average_rating] || 5} total_count={assigns[:total_count]} /> """ end # ── PDP blocks ────────────────────────────────────────────────── defp render_block(%{block: %{"type" => "breadcrumb"}} = assigns) do ~H""" <.breadcrumb :if={assigns[:product]} items={breadcrumb_items(assigns[:product])} mode={@mode} /> """ end defp render_block(%{block: %{"type" => "product_hero"}} = assigns) do ~H""" <%= if assigns[:product] do %>
<.product_gallery images={assigns[:gallery_images] || []} product_name={@product.title} />
<.product_info product={@product} display_price={assigns[:display_price]} />
<%= for option_type <- assigns[:option_types] || [] do %> <.variant_selector option_type={option_type} selected={(assigns[:selected_options] || %{})[option_type.name]} available={(assigns[:available_options] || %{})[option_type.name] || []} mode={@mode} option_urls={(assigns[:option_urls] || %{})[option_type.name] || %{}} /> <% end %>
One size
<.quantity_selector quantity={assigns[:quantity] || 1} in_stock={@product.in_stock} />

This product is no longer available

This option is currently unavailable

<.add_to_cart_button mode={@mode} text={ if assigns[:product_discontinued], do: "No longer available", else: "Add to basket" } disabled={ assigns[:product_discontinued] || (assigns[:selected_variant] && !assigns[:selected_variant].is_available) } />
<% end %> """ end defp render_block(%{block: %{"type" => "product_details"}} = assigns) do ~H""" <.product_details :if={assigns[:product]} product={@product} /> """ end defp render_block(%{block: %{"type" => "related_products"}} = assigns) do ~H""" <.related_products_section :if={@theme_settings.pdp_related_products && assigns[:related_products] != []} products={assigns[:related_products] || []} theme_settings={@theme_settings} mode={@mode} /> """ end # ── Collection blocks ─────────────────────────────────────────── defp render_block(%{block: %{"type" => "collection_header"}} = assigns) do count = if assigns[:pagination] do assigns.pagination.total_count else length(assigns[:products] || []) end assigns = assign(assigns, :product_count, count) ~H""" <.collection_header title={assigns[:collection_title] || "All Products"} product_count={@product_count} /> """ end defp render_block(%{block: %{"type" => "filter_bar"}} = assigns) do current_slug = case assigns[:current_category] do :sale -> "sale" nil -> nil cat -> cat.slug end assigns = assigns |> assign(:current_slug, current_slug) |> assign(:current_sort, assigns[:current_sort] || "featured") |> assign(:sort_options, assigns[:sort_options] || []) ~H"""
<.shop_select name="sort" options={@sort_options} selected={@current_sort} aria-label="Sort products" />
""" end defp render_block(%{block: %{"type" => "product_grid"}} = assigns) do show_category = assigns[:current_category] in [nil, :sale] collection_slug = assigns[:collection_slug] || "all" current_sort = assigns[:current_sort] || "featured" sort_params = if current_sort != "featured", do: %{"sort" => current_sort}, else: %{} assigns = assigns |> assign(:show_category, show_category) |> assign(:collection_slug, collection_slug) |> assign(:sort_params, sort_params) ~H"""
<.product_grid theme_settings={@theme_settings}> <%= for product <- assigns[:products] || [] do %> <.product_card product={product} theme_settings={@theme_settings} mode={@mode} variant={:default} show_category={@show_category} /> <% end %> <.shop_pagination :if={assigns[:pagination] && assigns[:pagination].total_pages > 1} page={assigns[:pagination]} base_path={~p"/collections/#{@collection_slug}"} params={@sort_params} /> <%= if (assigns[:products] || []) == [] do %>

No products found in this collection.

<.link patch={~p"/collections/all"} class="collection-empty-link"> View all products
<% end %>
""" end # ── Cart blocks ───────────────────────────────────────────────── defp render_block(%{block: %{"type" => "cart_items"}} = assigns) do ~H""" <.page_title text="Your basket" /> <%= if @cart_items == [] do %> <.cart_empty_state mode={@mode} /> <% else %> <% end %> """ end defp render_block(%{block: %{"type" => "order_summary"}} = assigns) do ~H""" <%= if @cart_items != [] do %> <.order_summary subtotal={assigns[:cart_page_subtotal] || 0} shipping_estimate={assigns[:shipping_estimate]} country_code={assigns[:country_code] || "GB"} available_countries={assigns[:available_countries] || []} stripe_connected={assigns[:stripe_connected] || false} mode={@mode} /> <% end %> """ end # ── Contact blocks ────────────────────────────────────────────── defp render_block(%{block: %{"type" => "contact_form"}} = assigns) do settings = assigns.block["settings"] || %{} assigns = assign(assigns, :email, settings["email"]) ~H""" <.contact_form email={@email} /> """ end defp render_block(%{block: %{"type" => "order_tracking_card"}} = assigns) do ~H""" <.order_tracking_card tracking_state={assigns[:tracking_state] || :idle} /> """ end # ── Content blocks ────────────────────────────────────────────── defp render_block(%{block: %{"type" => "content_body"}} = assigns) do settings = assigns.block["settings"] || %{} content = settings["content"] || "" {image_src, source_width, image_alt} = resolve_content_image(settings) assigns = assigns |> assign(:image_src, image_src) |> assign(:image_source_width, source_width || 1200) |> assign(:image_alt, image_alt) |> assign(:content, content) ~H"""
<%= if @image_src && @image_src != "" do %>
<.responsive_image src={@image_src} source_width={@image_source_width} alt={@image_alt} sizes="(max-width: 800px) 100vw, 800px" class="content-hero-image" />
<% end %> <%= if @content != "" do %> <%!-- Inline content from block settings — split on blank lines for paragraphs --%>

{para}

<% else %> <%!-- Structured rich text from LiveView (content pages) --%> <.rich_text blocks={assigns[:content_blocks] || []} /> <% end %>
""" end # ── Checkout success ──────────────────────────────────────────── defp render_block(%{block: %{"type" => "checkout_result"}} = assigns) do ~H""" <%= if assigns[:order] && assigns[:order].payment_status == "paid" do %>

Thank you for your order

Order {assigns[:order].order_number}

<%= if assigns[:order].customer_email do %>

A confirmation will be sent to {assigns[:order].customer_email}

<% end %>
<.shop_card class="checkout-card">

Order details

Total {Cart.format_price(assigns[:order].total)}
<%= if assigns[:order].shipping_address != %{} do %> <.shop_card class="checkout-card">

Shipping to

{assigns[:order].shipping_address["name"]}

{assigns[:order].shipping_address["line1"]}

<%= if assigns[:order].shipping_address["line2"] do %>

{assigns[:order].shipping_address["line2"]}

<% end %>

{assigns[:order].shipping_address["city"]}, {assigns[:order].shipping_address[ "postal_code" ]}

{assigns[:order].shipping_address["country"]}

<% end %>
<.shop_link_button href="/collections/all" class="checkout-cta"> Continue shopping
<% else %>

Processing your payment

Please wait while we confirm your payment. This usually takes a few seconds.

If this page doesn't update, please <.link patch="/contact" class="checkout-contact-link">contact us.

<% end %> """ end # ── Orders ────────────────────────────────────────────────────── defp render_block(%{block: %{"type" => "order_card"}} = assigns) do ~H"""

Your orders

<%= if assigns[:lookup_email] do %>

Orders for {assigns[:lookup_email]}

<% end %>
<%= cond do %> <% is_nil(assigns[:orders]) -> %>

This link has expired or is invalid.

Head back to the <.link patch="/contact">contact page to request a new one.

<% assigns[:orders] == [] -> %>

No orders found for that email address.

If something doesn't look right, <.link patch="/contact">get in touch.

<% true -> %>
<%= for order <- assigns[:orders] do %> <.link patch={"/orders/#{order.order_number}"} class="order-summary-card">

{order.order_number}

{Calendar.strftime(order.inserted_at, "%-d %B %Y")}

{format_order_status(order.fulfilment_status)}
<% end %>
<% end %> """ end # ── Order detail ──────────────────────────────────────────────── defp render_block(%{block: %{"type" => "order_detail_card"}} = assigns) do ~H""" <%= if assigns[:order] do %>
<.link patch="/orders" class="order-detail-back">← Back to orders

{assigns[:order].order_number}

{Calendar.strftime(assigns[:order].inserted_at, "%-d %B %Y")}

{format_order_status(assigns[:order].fulfilment_status)}
<%= if assigns[:order].tracking_number || assigns[:order].tracking_url do %> <.shop_card class="order-detail-tracking-card">

Shipment tracking

<%= if assigns[:order].tracking_number do %>

{assigns[:order].tracking_number}

<% end %>
<%= if assigns[:order].tracking_url do %> <.external_link href={assigns[:order].tracking_url} class="order-detail-tracking-btn themed-button" > Track parcel <% end %>
<% end %> <.shop_card class="checkout-card">

Items ordered

Total {Cart.format_price(assigns[:order].total)}
<%= if assigns[:order].shipping_address != %{} do %> <.shop_card class="checkout-card">

Shipping to

{assigns[:order].shipping_address["name"]}

{assigns[:order].shipping_address["line1"]}

<%= if assigns[:order].shipping_address["line2"] do %>

{assigns[:order].shipping_address["line2"]}

<% end %>

{assigns[:order].shipping_address["city"]}, {assigns[:order].shipping_address[ "postal_code" ]}

{assigns[:order].shipping_address["country"]}

<% end %>
<.shop_link_button href="/collections/all">Continue shopping
<% end %> """ end # ── Search ────────────────────────────────────────────────────── defp render_block(%{block: %{"type" => "search_results"}} = assigns) do ~H""" <.page_title text="Search" />
<%= if (assigns[:search_page_results] || []) != [] do %>

{length(assigns[:search_page_results])} {if length(assigns[:search_page_results]) == 1, do: "result", else: "results"} for "{assigns[:search_page_query]}"

<.product_grid theme_settings={@theme_settings}> <%= for product <- assigns[:search_page_results] do %> <.product_card product={product} theme_settings={@theme_settings} mode={@mode} variant={:default} /> <% end %> <% else %> <%= if (assigns[:search_page_query] || "") != "" do %>

No products found for “{assigns[:search_page_query]}”

<.link patch="/collections/all" class="collection-empty-link">Browse all products
<% end %> <% end %> """ end # ── Utility blocks ────────────────────────────────────────────── defp render_block(%{block: %{"type" => "spacer"}} = assigns) do size = get_in(assigns.block, ["settings", "size"]) || "medium" assigns = assign(assigns, :size, size) ~H""" """ end defp render_block(%{block: %{"type" => "divider"}} = assigns) do style = get_in(assigns.block, ["settings", "style"]) || "line" assigns = assign(assigns, :style, style) ~H"""
""" end defp render_block(%{block: %{"type" => "button"}} = assigns) do settings = assigns.block["settings"] || %{} assigns = assigns |> assign(:text, settings["text"] || "Learn more") |> assign(:href, settings["href"] || "/") |> assign(:btn_style, settings["style"] || "primary") |> assign(:alignment, settings["alignment"] || "centre") ~H"""
<.link patch={@href} class={if @btn_style == "outline", do: "themed-button-outline", else: "themed-button"} > {@text}
""" end defp render_block(%{block: %{"type" => "video_embed"}} = assigns) do settings = assigns.block["settings"] || %{} url = settings["url"] || "" caption = settings["caption"] || "" aspect_ratio = settings["aspect_ratio"] || "16:9" {provider, embed_url} = parse_video_url(url) assigns = assigns |> assign(:embed_url, embed_url) |> assign(:provider, provider) |> assign(:caption, caption) |> assign(:aspect_ratio, aspect_ratio) |> assign(:raw_url, url) ~H"""

<.external_link href={@raw_url}> {if @caption != "", do: @caption, else: "Watch video"}

{@caption}

""" end # ── Fallback ──────────────────────────────────────────────────── defp render_block(assigns) do ~H""" <%!-- Unknown block type: {assigns.block["type"]} --%> """ end # ── Helpers ───────────────────────────────────────────────────── @doc "Returns a CSS class for the page's `
` element." def page_main_class("contact"), do: "page-container contact-main" def page_main_class("cart"), do: "page-container" def page_main_class("checkout_success"), do: "page-container checkout-main" def page_main_class("orders"), do: "page-container orders-main" def page_main_class("order_detail"), do: "page-container order-detail-main" def page_main_class("error"), do: "error-main" def page_main_class("collection"), do: nil def page_main_class("pdp"), do: "page-container" def page_main_class("search"), do: "page-container" def page_main_class("about"), do: "content-page" def page_main_class("delivery"), do: "content-page" def page_main_class("privacy"), do: "content-page" def page_main_class("terms"), do: "content-page" def page_main_class(_), do: nil # Extracts the assigns that individual blocks need (everything except page metadata). # Preserves __changed__ so assign/3 works inside render_block functions. defp block_assigns(assigns) do Map.drop(assigns, [:page, :block_assigns]) end defp hero_variant("page"), do: :page defp hero_variant("sunken"), do: :default defp hero_variant("error"), do: :error defp hero_variant(_), do: :default defp hero_background("sunken"), do: :sunken defp hero_background(_), do: :base defp card_variant("minimal"), do: :minimal defp card_variant("compact"), do: :compact defp card_variant("default"), do: :default defp card_variant(_), do: :featured defp grid_columns("fixed-4"), do: :fixed_4 defp grid_columns(_), do: nil defp breadcrumb_items(%{category: cat, title: title}) when not is_nil(cat) do slug = cat |> String.downcase() |> String.replace(" ", "-") [ %{label: cat, page: "collection", href: "/collections/#{slug}"}, %{label: title, current: true} ] end defp breadcrumb_items(%{title: title}) do [%{label: title, current: true}] end defp breadcrumb_items(_), do: [] defp collection_path(slug, "featured"), do: ~p"/collections/#{slug}" defp collection_path(slug, sort), do: ~p"/collections/#{slug}?sort=#{sort}" # Resolves an image_id to a full URL for blocks that need a single URL (e.g. background-image). defp resolve_block_image_url(image_id) do case resolve_image(image_id) do nil -> "" image -> if image.is_svg do "/image_cache/#{image.id}.webp" else width = image.source_width |> Berrypod.Images.Optimizer.applicable_widths() |> List.last() "/image_cache/#{image.id}-#{width || 400}.webp" end end end # Resolves image_id for content_body blocks, returning {base_path, source_width, alt}. # base_path has no width/extension suffix — responsive_image adds those. defp resolve_content_image(settings) do case resolve_image(settings["image_id"]) do nil -> {nil, nil, ""} image -> {"/image_cache/#{image.id}", image.source_width, image.alt || image.filename} end end defp resolve_image(nil), do: nil defp resolve_image(""), do: nil defp resolve_image(image_id), do: Berrypod.Media.get_image(image_id) @youtube_re ~r/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/ @vimeo_re ~r/vimeo\.com\/(?:video\/)?(\d+)/ defp parse_video_url(url) when is_binary(url) do cond do match = Regex.run(@youtube_re, url) -> {:youtube, "https://www.youtube-nocookie.com/embed/#{Enum.at(match, 1)}"} match = Regex.run(@vimeo_re, url) -> {:vimeo, "https://player.vimeo.com/video/#{Enum.at(match, 1)}?dnt=1"} true -> {:unknown, nil} end end defp parse_video_url(_), do: {:unknown, nil} def format_order_status("unfulfilled"), do: "Being prepared" def format_order_status("submitted"), do: "Sent to printer" def format_order_status("processing"), do: "In production" def format_order_status("shipped"), do: "On its way" def format_order_status("delivered"), do: "Delivered" def format_order_status("failed"), do: "Issue — contact us" def format_order_status("cancelled"), do: "Cancelled" def format_order_status(status), do: status end