add live page editor sidebar with collapsible UI
All checks were successful
deploy / deploy (push) Successful in 6m49s
All checks were successful
deploy / deploy (push) Successful in 6m49s
Admins can now edit pages directly on the live shop by clicking the pencil icon in the header. A sidebar slides in with block management controls (add, remove, reorder, edit settings, save, reset, done). Key features: - PageEditorHook on_mount with handle_params/event/info hooks - BlockEditor pure functions extracted from admin editor - Shared BlockEditorComponents with event_prefix namespacing - Collapsible sidebar: X closes it, header pencil reopens it - Backdrop overlay dismisses sidebar on tap - Conditional admin.css loading for logged-in users - content_body block now portable (textarea setting + rich text fallback) 13 integration tests, 26 unit tests, 1370 total passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ defmodule BerrypodWeb.PageRenderer do
|
||||
router: BerrypodWeb.Router,
|
||||
statics: BerrypodWeb.static_paths()
|
||||
|
||||
import BerrypodWeb.BlockEditorComponents
|
||||
import BerrypodWeb.CoreComponents, only: [icon: 1]
|
||||
|
||||
alias Berrypod.Cart
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────
|
||||
@@ -24,8 +27,19 @@ defmodule BerrypodWeb.PageRenderer do
|
||||
|
||||
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
|
||||
if assigns[:editing] && assigns[:editing_blocks] do
|
||||
render_page_with_editor(assigns)
|
||||
else
|
||||
render_page_normal(assigns)
|
||||
end
|
||||
end
|
||||
|
||||
defp render_page_normal(assigns) do
|
||||
assigns = assign(assigns, :block_assigns, block_assigns(assigns))
|
||||
|
||||
~H"""
|
||||
@@ -43,6 +57,122 @@ defmodule BerrypodWeb.PageRenderer do
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_page_with_editor(assigns) do
|
||||
assigns = assign(assigns, :block_assigns, block_assigns(assigns))
|
||||
|
||||
~H"""
|
||||
<div
|
||||
id="page-editor-live"
|
||||
class="page-editor-live"
|
||||
phx-hook="DirtyGuard"
|
||||
data-dirty={to_string(@editor_dirty)}
|
||||
data-sidebar-open={to_string(@editor_sidebar_open)}
|
||||
>
|
||||
<aside class="page-editor-sidebar" aria-label="Page editor">
|
||||
<div class="page-editor-sidebar-header">
|
||||
<h2 class="page-editor-sidebar-title">{@page.title}</h2>
|
||||
<div class="page-editor-sidebar-actions">
|
||||
<button
|
||||
phx-click="editor_save"
|
||||
class={[
|
||||
"admin-btn admin-btn-sm admin-btn-primary",
|
||||
!@editor_dirty && "opacity-50"
|
||||
]}
|
||||
disabled={!@editor_dirty}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
phx-click="editor_reset_defaults"
|
||||
data-confirm="Reset this page to its default layout? Your changes will be lost."
|
||||
class="admin-btn admin-btn-sm admin-btn-ghost"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button phx-click="editor_done" class="admin-btn admin-btn-sm admin-btn-ghost">
|
||||
Done
|
||||
</button>
|
||||
<button
|
||||
phx-click="editor_toggle_sidebar"
|
||||
class="admin-btn admin-btn-sm admin-btn-ghost"
|
||||
aria-label="Close sidebar"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- ARIA live region for screen reader announcements --%>
|
||||
<div aria-live="polite" aria-atomic="true" class="sr-only">
|
||||
{if @editor_live_region_message, do: @editor_live_region_message}
|
||||
</div>
|
||||
|
||||
<%!-- Unsaved changes indicator --%>
|
||||
<p :if={@editor_dirty} class="admin-badge admin-badge-warning page-editor-sidebar-dirty">
|
||||
Unsaved changes
|
||||
</p>
|
||||
|
||||
<%!-- Block list --%>
|
||||
<div class="block-list" role="list" aria-label="Page blocks">
|
||||
<.block_card
|
||||
:for={{block, idx} <- Enum.with_index(@editing_blocks)}
|
||||
block={block}
|
||||
idx={idx}
|
||||
total={length(@editing_blocks)}
|
||||
expanded={@editor_expanded}
|
||||
event_prefix="editor_"
|
||||
/>
|
||||
|
||||
<div :if={@editing_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="editor_show_picker" class="admin-btn admin-btn-outline block-add-btn">
|
||||
<.icon name="hero-plus" class="size-4" /> Add block
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%!-- Block picker modal --%>
|
||||
<.block_picker
|
||||
:if={@editor_show_picker}
|
||||
allowed_blocks={@editor_allowed_blocks}
|
||||
filter={@editor_picker_filter}
|
||||
event_prefix="editor_"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<%!-- Backdrop: tapping the page dismisses the sidebar --%>
|
||||
<div
|
||||
:if={@editor_sidebar_open}
|
||||
class="page-editor-backdrop"
|
||||
phx-click="editor_toggle_sidebar"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div class="page-editor-content">
|
||||
<.shop_layout
|
||||
{layout_assigns(assigns)}
|
||||
active_page={@page.slug}
|
||||
error_page={@page.slug == "error"}
|
||||
>
|
||||
<main id="main-content" class={page_main_class(@page.slug)}>
|
||||
<div
|
||||
:for={block <- @editing_blocks}
|
||||
:key={block["id"]}
|
||||
data-block-type={block["type"]}
|
||||
>
|
||||
{render_block(Map.merge(@block_assigns, %{block: block, page_slug: @page.slug}))}
|
||||
</div>
|
||||
</main>
|
||||
</.shop_layout>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Block dispatch ──────────────────────────────────────────────
|
||||
|
||||
defp render_block(%{block: %{"type" => "hero"}} = assigns) do
|
||||
@@ -429,11 +559,13 @@ defmodule BerrypodWeb.PageRenderer do
|
||||
|
||||
defp render_block(%{block: %{"type" => "content_body"}} = assigns) do
|
||||
settings = assigns.block["settings"] || %{}
|
||||
content = settings["content"] || ""
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:image_src, settings["image_src"])
|
||||
|> assign(:image_alt, settings["image_alt"] || "")
|
||||
|> assign(:content, content)
|
||||
|
||||
~H"""
|
||||
<div class="content-body">
|
||||
@@ -449,7 +581,17 @@ defmodule BerrypodWeb.PageRenderer do
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<.rich_text blocks={assigns[:content_blocks] || []} />
|
||||
<%= if @content != "" do %>
|
||||
<%!-- Inline content from block settings — split on blank lines for paragraphs --%>
|
||||
<div class="rich-text">
|
||||
<p :for={para <- String.split(@content, ~r/\n{2,}/, trim: true)} class="rich-text-paragraph">
|
||||
{para}
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<%!-- Structured rich text from LiveView (content pages) --%>
|
||||
<.rich_text blocks={assigns[:content_blocks] || []} />
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user