add page builder polish: utility blocks, templates, duplicate
All checks were successful
deploy / deploy (push) Successful in 1m24s

New block types: spacer, divider, button/CTA, video embed (YouTube,
Vimeo with privacy-enhanced embeds, fallback for unknown URLs).

Page templates (blank, content, landing) shown when creating custom
pages. Duplicate page action on admin index with slug deduplication.

Fix block picker on shop edit sidebar being cut off on mobile by
accounting for bottom nav and making the grid scrollable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-28 17:33:25 +00:00
parent 69ccc625b2
commit 3336b3aa26
10 changed files with 686 additions and 2 deletions

View File

@@ -2,7 +2,7 @@ defmodule BerrypodWeb.Admin.Pages.CustomForm do
use BerrypodWeb, :live_view
alias Berrypod.Pages
alias Berrypod.Pages.Page
alias Berrypod.Pages.{Defaults, Page}
@impl true
def mount(params, _session, socket) do
@@ -16,6 +16,7 @@ defmodule BerrypodWeb.Admin.Pages.CustomForm do
|> assign(:page_title, "New page")
|> assign(:page_struct, %Page{})
|> assign(:slug_touched, false)
|> assign(:selected_template, "blank")
|> assign(:form, to_form(changeset))
end
@@ -53,12 +54,20 @@ defmodule BerrypodWeb.Admin.Pages.CustomForm do
{:noreply, assign(socket, form: form, slug_touched: slug_touched)}
end
@impl true
def handle_event("select_template", %{"key" => key}, socket) do
{:noreply, assign(socket, :selected_template, key)}
end
@impl true
def handle_event("save", %{"page" => params}, socket) do
save_page(socket, socket.assigns.live_action, params)
end
defp save_page(socket, :new, params) do
template_blocks = Defaults.template_blocks(socket.assigns.selected_template)
params = Map.put(params, "blocks", template_blocks)
case Pages.create_custom_page(params) do
{:ok, page} ->
{:noreply,
@@ -120,6 +129,25 @@ defmodule BerrypodWeb.Admin.Pages.CustomForm do
{if @live_action == :new, do: "New page", else: "Page settings"}
</.header>
<div :if={@live_action == :new} class="template-picker">
<p class="template-picker-label">Start from a template</p>
<div class="template-picker-cards">
<button
:for={tmpl <- Defaults.templates()}
type="button"
phx-click="select_template"
phx-value-key={tmpl.key}
class={[
"template-card",
assigns[:selected_template] == tmpl.key && "template-card-selected"
]}
>
<span class="template-card-name">{tmpl.label}</span>
<span class="template-card-desc">{tmpl.description}</span>
</button>
</div>
</div>
<.form
for={@form}
id="custom-page-form"

View File

@@ -40,6 +40,26 @@ defmodule BerrypodWeb.Admin.Pages.Index do
end
end
@impl true
def handle_event("duplicate_custom_page", %{"slug" => slug}, socket) do
case Pages.get_page_struct(slug) do
%{type: "custom"} = page ->
case Pages.duplicate_custom_page(page) do
{:ok, copy} ->
{:noreply,
socket
|> assign(:custom_pages, Pages.list_custom_pages())
|> put_flash(:info, "\"#{copy.title}\" created as draft")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to duplicate page")}
end
_ ->
{:noreply, put_flash(socket, :error, "Page not found")}
end
end
@impl true
def render(assigns) do
~H"""
@@ -101,6 +121,14 @@ defmodule BerrypodWeb.Admin.Pages.Index do
</span>
<.icon name="hero-chevron-right-mini" class="size-4 page-card-arrow" />
</.link>
<button
phx-click="duplicate_custom_page"
phx-value-slug={page.slug}
class="page-card-action"
aria-label={"Duplicate #{page.title}"}
>
<.icon name="hero-document-duplicate" class="size-4" />
</button>
<button
phx-click="delete_custom_page"
phx-value-slug={page.slug}