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>
163 lines
5.5 KiB
Elixir
163 lines
5.5 KiB
Elixir
defmodule BerrypodWeb.Admin.Pages.Index do
|
|
use BerrypodWeb, :live_view
|
|
|
|
alias Berrypod.Pages
|
|
|
|
@page_groups [
|
|
{"Marketing", ~w(home about contact)},
|
|
{"Legal", ~w(delivery privacy terms)},
|
|
{"Shop", ~w(collection pdp cart search)},
|
|
{"Orders", ~w(checkout_success orders order_detail)},
|
|
{"System", ~w(error)}
|
|
]
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
pages = Pages.list_pages() |> Map.new(&{&1.slug, &1})
|
|
custom_pages = Pages.list_custom_pages()
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, "Pages")
|
|
|> assign(:pages, pages)
|
|
|> assign(:custom_pages, custom_pages)
|
|
|> assign(:page_groups, @page_groups)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("delete_custom_page", %{"slug" => slug}, socket) do
|
|
case Pages.get_page_struct(slug) do
|
|
%{type: "custom"} = page ->
|
|
{:ok, _} = Pages.delete_custom_page(page)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:custom_pages, Pages.list_custom_pages())
|
|
|> put_flash(:info, "Page deleted")}
|
|
|
|
_ ->
|
|
{:noreply, put_flash(socket, :error, "Page not found")}
|
|
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"""
|
|
<.header>
|
|
Pages
|
|
<:subtitle>Customise the layout and content of every page on your shop.</:subtitle>
|
|
<:actions>
|
|
<.link navigate={~p"/admin/pages/new"} class="admin-btn admin-btn-sm admin-btn-primary">
|
|
<.icon name="hero-plus" class="size-4" /> New page
|
|
</.link>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<div class="page-list">
|
|
<div :for={{group_name, slugs} <- @page_groups} class="page-group">
|
|
<h3 class="page-group-title">{group_name}</h3>
|
|
<div class="page-group-cards">
|
|
<.link
|
|
:for={slug <- slugs}
|
|
navigate={~p"/admin/pages/#{slug}"}
|
|
class="page-card"
|
|
>
|
|
<span class="page-card-icon">
|
|
<.icon name={page_icon(slug)} class="size-5" />
|
|
</span>
|
|
<span class="page-card-info">
|
|
<span class="page-card-title">{@pages[slug].title}</span>
|
|
<span class="page-card-meta">
|
|
{length(@pages[slug].blocks)} {if length(@pages[slug].blocks) == 1,
|
|
do: "block",
|
|
else: "blocks"}
|
|
</span>
|
|
</span>
|
|
<.icon name="hero-chevron-right-mini" class="size-4 page-card-arrow" />
|
|
</.link>
|
|
</div>
|
|
</div>
|
|
|
|
<div :if={@custom_pages != []} class="page-group">
|
|
<h3 class="page-group-title">Custom pages</h3>
|
|
<div class="page-group-cards">
|
|
<div :for={page <- @custom_pages} class="page-card page-card-custom">
|
|
<.link navigate={~p"/admin/pages/#{page.slug}"} class="page-card-link">
|
|
<span class="page-card-icon">
|
|
<.icon name="hero-document" class="size-5" />
|
|
</span>
|
|
<span class="page-card-info">
|
|
<span class="page-card-title">
|
|
{page.title}
|
|
<span :if={!page.published} class="admin-badge admin-badge-warning ml-2">
|
|
Draft
|
|
</span>
|
|
</span>
|
|
<span class="page-card-meta">
|
|
/{page.slug} · {length(page.blocks)} {if length(page.blocks) == 1,
|
|
do: "block",
|
|
else: "blocks"}
|
|
</span>
|
|
</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}
|
|
data-confirm={"Delete \"#{page.title}\"? This cannot be undone."}
|
|
class="page-card-delete"
|
|
aria-label={"Delete #{page.title}"}
|
|
>
|
|
<.icon name="hero-trash" class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
defp page_icon("home"), do: "hero-home"
|
|
defp page_icon("about"), do: "hero-user"
|
|
defp page_icon("contact"), do: "hero-envelope"
|
|
defp page_icon("delivery"), do: "hero-truck"
|
|
defp page_icon("privacy"), do: "hero-shield-check"
|
|
defp page_icon("terms"), do: "hero-document-text"
|
|
defp page_icon("collection"), do: "hero-tag"
|
|
defp page_icon("pdp"), do: "hero-cube"
|
|
defp page_icon("cart"), do: "hero-shopping-cart"
|
|
defp page_icon("search"), do: "hero-magnifying-glass"
|
|
defp page_icon("checkout_success"), do: "hero-check-circle"
|
|
defp page_icon("orders"), do: "hero-clipboard-document-list"
|
|
defp page_icon("order_detail"), do: "hero-clipboard-document"
|
|
defp page_icon("error"), do: "hero-exclamation-triangle"
|
|
end
|