add live preview pane to page editor
All checks were successful
deploy / deploy (push) Successful in 4m55s
All checks were successful
deploy / deploy (push) Successful in 4m55s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,22 @@
|
||||
defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.Pages
|
||||
alias Berrypod.Pages.{BlockTypes, Defaults, SettingsField}
|
||||
alias Berrypod.{Media, Pages}
|
||||
alias Berrypod.Pages.{BlockTypes, Defaults}
|
||||
alias Berrypod.Products.ProductImage
|
||||
alias Berrypod.Theme.{Fonts, PreviewData}
|
||||
|
||||
@impl true
|
||||
def mount(%{"slug" => slug}, _session, socket) do
|
||||
page = Pages.get_page(slug)
|
||||
allowed_blocks = BlockTypes.allowed_for(slug)
|
||||
|
||||
preview_data = %{
|
||||
products: PreviewData.products(),
|
||||
categories: PreviewData.categories(),
|
||||
cart_items: PreviewData.cart_items()
|
||||
}
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, page.title)
|
||||
@@ -20,7 +28,11 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
|> assign(:show_picker, false)
|
||||
|> assign(:picker_filter, "")
|
||||
|> assign(:expanded, MapSet.new())
|
||||
|> assign(:live_region_message, nil)}
|
||||
|> assign(:live_region_message, nil)
|
||||
|> assign(:show_preview, false)
|
||||
|> assign(:preview_data, preview_data)
|
||||
|> assign(:logo_image, Media.get_logo())
|
||||
|> assign(:header_image, Media.get_header())}
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -313,6 +325,10 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
|> put_flash(:info, "Page reset to defaults")}
|
||||
end
|
||||
|
||||
def handle_event("toggle_preview", _params, socket) do
|
||||
{:noreply, assign(socket, :show_preview, !socket.assigns.show_preview)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
@@ -326,6 +342,16 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
<.header>
|
||||
{@page_data.title}
|
||||
<:actions>
|
||||
<button
|
||||
phx-click="toggle_preview"
|
||||
class="admin-btn admin-btn-sm admin-btn-ghost page-editor-toggle-preview"
|
||||
>
|
||||
<.icon
|
||||
name={if @show_preview, do: "hero-pencil-square", else: "hero-eye"}
|
||||
class="size-4"
|
||||
/>
|
||||
{if @show_preview, do: "Edit", else: "Preview"}
|
||||
</button>
|
||||
<button
|
||||
phx-click="reset_defaults"
|
||||
data-confirm="Reset this page to its default layout? Your changes will be lost."
|
||||
@@ -353,26 +379,51 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
Unsaved changes
|
||||
</p>
|
||||
|
||||
<%!-- Block list --%>
|
||||
<div class="block-list" role="list" aria-label="Page blocks">
|
||||
<.block_card
|
||||
:for={{block, idx} <- Enum.with_index(@blocks)}
|
||||
block={block}
|
||||
idx={idx}
|
||||
total={length(@blocks)}
|
||||
expanded={@expanded}
|
||||
/>
|
||||
<div class="page-editor-container">
|
||||
<%!-- Editor pane --%>
|
||||
<div class={[
|
||||
"page-editor-pane",
|
||||
@show_preview && "page-editor-pane-hidden-mobile"
|
||||
]}>
|
||||
<%!-- Block list --%>
|
||||
<div class="block-list" role="list" aria-label="Page blocks">
|
||||
<.block_card
|
||||
:for={{block, idx} <- Enum.with_index(@blocks)}
|
||||
block={block}
|
||||
idx={idx}
|
||||
total={length(@blocks)}
|
||||
expanded={@expanded}
|
||||
/>
|
||||
|
||||
<div :if={@blocks == []} class="block-list-empty">
|
||||
<p>No blocks on this page yet.</p>
|
||||
<div :if={@blocks == []} class="block-list-empty">
|
||||
<p>No blocks on this page yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Add block button --%>
|
||||
<div class="block-actions">
|
||||
<button phx-click="show_picker" class="admin-btn admin-btn-outline block-add-btn">
|
||||
<.icon name="hero-plus" class="size-4" /> Add block
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Add block button --%>
|
||||
<div class="block-actions">
|
||||
<button phx-click="show_picker" class="admin-btn admin-btn-outline block-add-btn">
|
||||
<.icon name="hero-plus" class="size-4" /> Add block
|
||||
</button>
|
||||
<%!-- Preview pane --%>
|
||||
<div class={[
|
||||
"page-editor-preview-pane",
|
||||
!@show_preview && "page-editor-preview-hidden-mobile"
|
||||
]}>
|
||||
<.preview_pane
|
||||
slug={@slug}
|
||||
blocks={@blocks}
|
||||
page_data={@page_data}
|
||||
preview_data={@preview_data}
|
||||
theme_settings={@theme_settings}
|
||||
generated_css={@generated_css}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Block picker modal --%>
|
||||
@@ -385,6 +436,126 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Preview ───────────────────────────────────────────────────────
|
||||
|
||||
defp preview_pane(assigns) do
|
||||
# Build a temporary page struct from working state
|
||||
page = %{slug: assigns.slug, title: assigns.page_data.title, blocks: assigns.blocks}
|
||||
|
||||
preview =
|
||||
assigns
|
||||
|> assign(:page, page)
|
||||
|> assign(:mode, :preview)
|
||||
|> assign(:products, assigns.preview_data.products)
|
||||
|> assign(:categories, assigns.preview_data.categories)
|
||||
|> assign(:cart_items, PreviewData.cart_drawer_items())
|
||||
|> assign(:cart_count, 2)
|
||||
|> assign(:cart_subtotal, "£72.00")
|
||||
|> assign(:cart_drawer_open, false)
|
||||
|> preview_page_context(assigns.slug)
|
||||
|
||||
extra = Pages.load_block_data(page.blocks, preview)
|
||||
assigns = assign(assigns, :preview, assign(preview, extra))
|
||||
|
||||
~H"""
|
||||
<div
|
||||
class="page-editor-preview themed"
|
||||
data-mood={@theme_settings.mood}
|
||||
data-typography={@theme_settings.typography}
|
||||
data-shape={@theme_settings.shape}
|
||||
data-density={@theme_settings.density}
|
||||
data-grid={@theme_settings.grid_columns}
|
||||
data-header={@theme_settings.header_layout}
|
||||
data-sticky={to_string(@theme_settings.sticky_header)}
|
||||
data-layout={@theme_settings.layout_width}
|
||||
data-shadow={@theme_settings.card_shadow}
|
||||
data-button-style={@theme_settings.button_style}
|
||||
>
|
||||
<style>
|
||||
<%= Phoenix.HTML.raw(Fonts.generate_all_font_faces(&BerrypodWeb.Endpoint.static_path/1)) %>
|
||||
<%= Phoenix.HTML.raw(@generated_css) %>
|
||||
</style>
|
||||
<BerrypodWeb.PageRenderer.render_page {@preview} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp preview_page_context(assigns, "pdp") do
|
||||
product = List.first(assigns.preview_data.products)
|
||||
option_types = Map.get(product, :option_types) || []
|
||||
variants = Map.get(product, :variants) || []
|
||||
|
||||
{selected_options, selected_variant} =
|
||||
case variants do
|
||||
[first | _] -> {first.options, first}
|
||||
[] -> {%{}, nil}
|
||||
end
|
||||
|
||||
available_options =
|
||||
Enum.reduce(option_types, %{}, fn opt, acc ->
|
||||
values = Enum.map(opt.values, & &1.title)
|
||||
Map.put(acc, opt.name, values)
|
||||
end)
|
||||
|
||||
display_price =
|
||||
if selected_variant, do: selected_variant.price, else: product.cheapest_price
|
||||
|
||||
assigns
|
||||
|> assign(:product, product)
|
||||
|> assign(:gallery_images, build_gallery_images(product))
|
||||
|> assign(:option_types, option_types)
|
||||
|> assign(:selected_options, selected_options)
|
||||
|> assign(:available_options, available_options)
|
||||
|> assign(:display_price, display_price)
|
||||
|> assign(:quantity, 1)
|
||||
|> assign(:option_urls, %{})
|
||||
end
|
||||
|
||||
defp preview_page_context(assigns, "cart") do
|
||||
cart_items = assigns.preview_data.cart_items
|
||||
|
||||
subtotal =
|
||||
Enum.reduce(cart_items, 0, fn item, acc ->
|
||||
acc + item.product.cheapest_price * item.quantity
|
||||
end)
|
||||
|
||||
assigns
|
||||
|> assign(:cart_page_items, cart_items)
|
||||
|> assign(:cart_page_subtotal, subtotal)
|
||||
end
|
||||
|
||||
defp preview_page_context(assigns, "about"),
|
||||
do: assign(assigns, :content_blocks, PreviewData.about_content())
|
||||
|
||||
defp preview_page_context(assigns, "delivery"),
|
||||
do: assign(assigns, :content_blocks, PreviewData.delivery_content())
|
||||
|
||||
defp preview_page_context(assigns, "privacy"),
|
||||
do: assign(assigns, :content_blocks, PreviewData.privacy_content())
|
||||
|
||||
defp preview_page_context(assigns, "terms"),
|
||||
do: assign(assigns, :content_blocks, PreviewData.terms_content())
|
||||
|
||||
defp preview_page_context(assigns, "error") do
|
||||
assign(assigns, %{
|
||||
error_code: "404",
|
||||
error_title: "Page Not Found",
|
||||
error_description:
|
||||
"Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved."
|
||||
})
|
||||
end
|
||||
|
||||
defp preview_page_context(assigns, _slug), do: assigns
|
||||
|
||||
defp build_gallery_images(product) do
|
||||
(Map.get(product, :images) || [])
|
||||
|> Enum.sort_by(& &1.position)
|
||||
|> Enum.map(fn img -> ProductImage.url(img, 1200) end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
# ── Block card ─────────────────────────────────────────────────────
|
||||
|
||||
defp block_card(assigns) do
|
||||
block_type = BlockTypes.get(assigns.block["type"])
|
||||
has_settings = has_settings?(assigns.block)
|
||||
|
||||
Reference in New Issue
Block a user