berrypod/lib/berrypod_web/live/admin/pages/custom_form.ex
jamey 3336b3aa26
All checks were successful
deploy / deploy (push) Successful in 1m24s
add page builder polish: utility blocks, templates, duplicate
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>
2026-02-28 17:33:25 +00:00

187 lines
5.4 KiB
Elixir

defmodule BerrypodWeb.Admin.Pages.CustomForm do
use BerrypodWeb, :live_view
alias Berrypod.Pages
alias Berrypod.Pages.{Defaults, Page}
@impl true
def mount(params, _session, socket) do
{:ok, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :new, _params) do
changeset = Page.custom_changeset(%Page{}, %{})
socket
|> assign(:page_title, "New page")
|> assign(:page_struct, %Page{})
|> assign(:slug_touched, false)
|> assign(:selected_template, "blank")
|> assign(:form, to_form(changeset))
end
defp apply_action(socket, :edit, %{"slug" => slug}) do
case Pages.get_page_struct(slug) do
%Page{type: "custom"} = page ->
changeset = Page.custom_changeset(page, %{})
socket
|> assign(:page_title, "#{page.title} settings")
|> assign(:page_struct, page)
|> assign(:slug_touched, true)
|> assign(:form, to_form(changeset))
_ ->
socket
|> put_flash(:error, "Page not found")
|> push_navigate(to: ~p"/admin/pages")
end
end
@impl true
def handle_event("validate", %{"page" => params}, socket) do
params = maybe_slugify(params, socket)
form =
socket.assigns.page_struct
|> Page.custom_changeset(params)
|> Map.put(:action, :validate)
|> to_form()
slug_touched =
socket.assigns.slug_touched || (params["slug"] && params["slug"] != "")
{: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,
socket
|> put_flash(:info, "Page created")
|> push_navigate(to: ~p"/admin/pages/#{page.slug}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_page(socket, :edit, params) do
case Pages.update_custom_page(socket.assigns.page_struct, params) do
{:ok, page} ->
{:noreply,
socket
|> put_flash(:info, "Page settings saved")
|> push_navigate(to: ~p"/admin/pages/#{page.slug}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
# Auto-generate slug from title when creating and slug hasn't been manually edited
defp maybe_slugify(params, %{assigns: %{live_action: :new, slug_touched: false}}) do
case params["title"] do
title when is_binary(title) and title != "" ->
slug =
title
|> String.downcase()
|> String.replace(~r/[^a-z0-9\s-]/, "")
|> String.trim()
|> String.replace(~r/\s+/, "-")
|> String.replace(~r/-+/, "-")
|> String.trim("-")
Map.put(params, "slug", slug)
_ ->
params
end
end
defp maybe_slugify(params, _socket), do: params
@impl true
def render(assigns) do
~H"""
<.link
navigate={~p"/admin/pages"}
class="text-sm font-normal text-base-content/60 hover:underline"
>
&larr; Pages
</.link>
<.header>
{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"
phx-change="validate"
phx-submit="save"
class="mt-6 space-y-6"
style="max-width: 32rem;"
>
<.input field={@form[:title]} label="Title" />
<.input field={@form[:slug]} label="URL slug" />
<.input
field={@form[:meta_description]}
type="textarea"
label="Meta description"
phx-no-feedback
/>
<.input field={@form[:published]} type="checkbox" label="Published" />
<.input field={@form[:show_in_nav]} type="checkbox" label="Show in navigation" />
<div :if={@form[:show_in_nav].value == true} class="space-y-4 pl-6">
<.input field={@form[:nav_label]} label="Nav label" />
<.input field={@form[:nav_position]} type="number" label="Nav position" />
</div>
<div class="flex gap-3">
<.button type="submit" phx-disable-with="Saving...">
{if @live_action == :new, do: "Create page", else: "Save settings"}
</.button>
<.link navigate={~p"/admin/pages"} class="admin-btn admin-btn-ghost">
Cancel
</.link>
</div>
</.form>
"""
end
end