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>
187 lines
5.4 KiB
Elixir
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"
|
|
>
|
|
← 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
|