add admin UX polish: nav grouping, inline settings, real preview data
All checks were successful
deploy / deploy (push) Successful in 1m33s
All checks were successful
deploy / deploy (push) Successful in 1m33s
- sidebar nav grouped under Shop/Content/Settings section headers with subtle uppercase labels (#105) - custom page settings now show inline in a collapsible panel within the editor instead of navigating away to a separate page (#107) - admin editor preview loads real products and categories from the DB, falling back to PreviewData only on fresh installs (#108) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
32cd642110
commit
0a7982dfe8
@ -94,6 +94,22 @@
|
||||
|
||||
/* ── Sidebar nav ── */
|
||||
|
||||
.admin-nav-group {
|
||||
& + & {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-nav-heading {
|
||||
display: block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: color-mix(in oklch, var(--t-text-primary) 45%, transparent);
|
||||
}
|
||||
|
||||
.admin-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@ -171,6 +187,10 @@
|
||||
&:hover { background-color: var(--t-surface-sunken); opacity: 1; }
|
||||
}
|
||||
|
||||
.admin-btn-active {
|
||||
background-color: var(--t-surface-sunken);
|
||||
}
|
||||
|
||||
.admin-btn-outline {
|
||||
background: none;
|
||||
color: inherit;
|
||||
@ -1282,6 +1302,39 @@
|
||||
color: var(--t-text-secondary);
|
||||
}
|
||||
|
||||
/* Inline page settings panel */
|
||||
|
||||
.page-settings-panel {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--t-border-default);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--t-surface-sunken);
|
||||
}
|
||||
|
||||
.page-settings-fields {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.page-settings-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-settings-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--t-border-default);
|
||||
}
|
||||
|
||||
/* Page editor split layout */
|
||||
|
||||
.page-editor-container {
|
||||
|
||||
@ -53,6 +53,8 @@
|
||||
|
||||
<%!-- nav links --%>
|
||||
<nav class="flex-1 p-2" aria-label="Admin navigation">
|
||||
<div class="admin-nav-group">
|
||||
<span class="admin-nav-heading">Shop</span>
|
||||
<ul class="admin-nav">
|
||||
<li>
|
||||
<.link
|
||||
@ -94,6 +96,12 @@
|
||||
<.icon name="hero-link" class="size-5" /> Print providers
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav-group">
|
||||
<span class="admin-nav-heading">Content</span>
|
||||
<ul class="admin-nav">
|
||||
<li>
|
||||
<.link
|
||||
navigate={~p"/admin/pages"}
|
||||
@ -126,6 +134,12 @@
|
||||
<.icon name="hero-paint-brush" class="size-5" /> Theme
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav-group">
|
||||
<span class="admin-nav-heading">Settings</span>
|
||||
<ul class="admin-nav">
|
||||
<li>
|
||||
<.link
|
||||
navigate={~p"/admin/settings"}
|
||||
@ -151,6 +165,7 @@
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<%!-- sidebar footer --%>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.{LegalPages, Media, Pages}
|
||||
alias Berrypod.{LegalPages, Media, Pages, Products}
|
||||
alias Berrypod.Pages.{BlockEditor, BlockTypes, Page}
|
||||
alias Berrypod.Products.ProductImage
|
||||
alias Berrypod.Theme.{Fonts, PreviewData}
|
||||
@ -13,9 +13,12 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
page = Pages.get_page(slug)
|
||||
allowed_blocks = BlockTypes.allowed_for(slug)
|
||||
|
||||
real_products = Products.list_visible_products(limit: 8)
|
||||
real_categories = Products.list_categories()
|
||||
|
||||
preview_data = %{
|
||||
products: PreviewData.products(),
|
||||
categories: PreviewData.categories(),
|
||||
products: if(real_products != [], do: real_products, else: PreviewData.products()),
|
||||
categories: if(real_categories != [], do: real_categories, else: PreviewData.categories()),
|
||||
cart_items: PreviewData.cart_items()
|
||||
}
|
||||
|
||||
@ -43,6 +46,7 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
|> assign(:image_picker_search, "")
|
||||
|> assign(:is_custom_page, !Page.system_slug?(slug))
|
||||
|> assign(:is_legal_page, LegalPages.legal_slug?(slug))
|
||||
|> assign_settings_form(slug)
|
||||
|> allow_upload(:image_picker_upload,
|
||||
accept: ~w(.png .jpg .jpeg .webp .svg),
|
||||
max_entries: 1,
|
||||
@ -462,6 +466,55 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
end
|
||||
end
|
||||
|
||||
# ── Page settings (custom pages only) ─────────────────────────────
|
||||
|
||||
def handle_event("toggle_settings", _params, socket) do
|
||||
{:noreply, assign(socket, :show_settings, !socket.assigns.show_settings)}
|
||||
end
|
||||
|
||||
def handle_event("validate_settings", %{"page" => params}, socket) do
|
||||
form =
|
||||
socket.assigns.page_struct
|
||||
|> Page.custom_changeset(params)
|
||||
|> Map.put(:action, :validate)
|
||||
|> to_form()
|
||||
|
||||
{:noreply, assign(socket, :settings_form, form)}
|
||||
end
|
||||
|
||||
def handle_event("save_settings", %{"page" => params}, socket) do
|
||||
case Pages.update_custom_page(socket.assigns.page_struct, params) do
|
||||
{:ok, page} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_struct, page)
|
||||
|> assign(:page_data, %{socket.assigns.page_data | title: page.title})
|
||||
|> assign(:page_title, page.title)
|
||||
|> assign(:settings_form, to_form(Page.custom_changeset(page, %{})))
|
||||
|> assign(:show_settings, false)
|
||||
|> put_flash(:info, "Page settings saved")}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:noreply, assign(socket, :settings_form, to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_settings_form(socket, slug) do
|
||||
if Page.system_slug?(slug) do
|
||||
socket
|
||||
|> assign(:show_settings, false)
|
||||
|> assign(:page_struct, nil)
|
||||
|> assign(:settings_form, nil)
|
||||
else
|
||||
page_struct = Pages.get_page_struct(slug)
|
||||
|
||||
socket
|
||||
|> assign(:show_settings, false)
|
||||
|> assign(:page_struct, page_struct)
|
||||
|> assign(:settings_form, to_form(Page.custom_changeset(page_struct, %{})))
|
||||
end
|
||||
end
|
||||
|
||||
# ── Render ───────────────────────────────────────────────────────
|
||||
|
||||
@impl true
|
||||
@ -487,13 +540,16 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
/>
|
||||
{if @show_preview, do: "Edit", else: "Preview"}
|
||||
</button>
|
||||
<.link
|
||||
<button
|
||||
:if={@is_custom_page}
|
||||
navigate={~p"/admin/pages/#{@slug}/settings"}
|
||||
class="admin-btn admin-btn-sm admin-btn-ghost"
|
||||
phx-click="toggle_settings"
|
||||
class={[
|
||||
"admin-btn admin-btn-sm admin-btn-ghost",
|
||||
@show_settings && "admin-btn-active"
|
||||
]}
|
||||
>
|
||||
<.icon name="hero-cog-6-tooth" class="size-4" /> Settings
|
||||
</.link>
|
||||
</button>
|
||||
<button
|
||||
phx-click="undo"
|
||||
class={["admin-btn admin-btn-sm admin-btn-ghost", @history == [] && "opacity-30"]}
|
||||
@ -554,6 +610,47 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- Inline page settings (custom pages) --%>
|
||||
<div :if={@show_settings && @settings_form} class="page-settings-panel">
|
||||
<.form
|
||||
for={@settings_form}
|
||||
id="inline-page-settings"
|
||||
phx-change="validate_settings"
|
||||
phx-submit="save_settings"
|
||||
>
|
||||
<div class="page-settings-fields">
|
||||
<.input field={@settings_form[:title]} label="Title" />
|
||||
<.input field={@settings_form[:slug]} label="URL slug" />
|
||||
<.input
|
||||
field={@settings_form[:meta_description]}
|
||||
type="textarea"
|
||||
label="Meta description"
|
||||
phx-no-feedback
|
||||
/>
|
||||
<div class="page-settings-row">
|
||||
<.input field={@settings_form[:published]} type="checkbox" label="Published" />
|
||||
<.input
|
||||
field={@settings_form[:show_in_nav]}
|
||||
type="checkbox"
|
||||
label="Show in navigation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-settings-actions">
|
||||
<.button type="submit" phx-disable-with="Saving..." class="admin-btn-sm">
|
||||
Save settings
|
||||
</.button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="toggle_settings"
|
||||
class="admin-btn admin-btn-ghost admin-btn-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
|
||||
<div class="page-editor-container">
|
||||
<%!-- Editor pane --%>
|
||||
<div class={[
|
||||
|
||||
@ -987,7 +987,7 @@ defmodule BerrypodWeb.Admin.PagesTest do
|
||||
test "shows settings button for custom pages", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/pages/size-guide")
|
||||
|
||||
assert has_element?(view, "a[href='/admin/pages/size-guide/settings']", "Settings")
|
||||
assert has_element?(view, "button", "Settings")
|
||||
end
|
||||
|
||||
test "does not show reset to defaults for custom pages", %{conn: conn} do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user