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 ── */
|
/* ── 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 {
|
.admin-nav {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -171,6 +187,10 @@
|
|||||||
&:hover { background-color: var(--t-surface-sunken); opacity: 1; }
|
&:hover { background-color: var(--t-surface-sunken); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-btn-active {
|
||||||
|
background-color: var(--t-surface-sunken);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-btn-outline {
|
.admin-btn-outline {
|
||||||
background: none;
|
background: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@ -1282,6 +1302,39 @@
|
|||||||
color: var(--t-text-secondary);
|
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 split layout */
|
||||||
|
|
||||||
.page-editor-container {
|
.page-editor-container {
|
||||||
|
|||||||
@ -53,6 +53,8 @@
|
|||||||
|
|
||||||
<%!-- nav links --%>
|
<%!-- nav links --%>
|
||||||
<nav class="flex-1 p-2" aria-label="Admin navigation">
|
<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">
|
<ul class="admin-nav">
|
||||||
<li>
|
<li>
|
||||||
<.link
|
<.link
|
||||||
@ -94,6 +96,12 @@
|
|||||||
<.icon name="hero-link" class="size-5" /> Print providers
|
<.icon name="hero-link" class="size-5" /> Print providers
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-nav-group">
|
||||||
|
<span class="admin-nav-heading">Content</span>
|
||||||
|
<ul class="admin-nav">
|
||||||
<li>
|
<li>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/admin/pages"}
|
navigate={~p"/admin/pages"}
|
||||||
@ -126,6 +134,12 @@
|
|||||||
<.icon name="hero-paint-brush" class="size-5" /> Theme
|
<.icon name="hero-paint-brush" class="size-5" /> Theme
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-nav-group">
|
||||||
|
<span class="admin-nav-heading">Settings</span>
|
||||||
|
<ul class="admin-nav">
|
||||||
<li>
|
<li>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/admin/settings"}
|
navigate={~p"/admin/settings"}
|
||||||
@ -151,6 +165,7 @@
|
|||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<%!-- sidebar footer --%>
|
<%!-- sidebar footer --%>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
defmodule BerrypodWeb.Admin.Pages.Editor do
|
defmodule BerrypodWeb.Admin.Pages.Editor do
|
||||||
use BerrypodWeb, :live_view
|
use BerrypodWeb, :live_view
|
||||||
|
|
||||||
alias Berrypod.{LegalPages, Media, Pages}
|
alias Berrypod.{LegalPages, Media, Pages, Products}
|
||||||
alias Berrypod.Pages.{BlockEditor, BlockTypes, Page}
|
alias Berrypod.Pages.{BlockEditor, BlockTypes, Page}
|
||||||
alias Berrypod.Products.ProductImage
|
alias Berrypod.Products.ProductImage
|
||||||
alias Berrypod.Theme.{Fonts, PreviewData}
|
alias Berrypod.Theme.{Fonts, PreviewData}
|
||||||
@ -13,9 +13,12 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
page = Pages.get_page(slug)
|
page = Pages.get_page(slug)
|
||||||
allowed_blocks = BlockTypes.allowed_for(slug)
|
allowed_blocks = BlockTypes.allowed_for(slug)
|
||||||
|
|
||||||
|
real_products = Products.list_visible_products(limit: 8)
|
||||||
|
real_categories = Products.list_categories()
|
||||||
|
|
||||||
preview_data = %{
|
preview_data = %{
|
||||||
products: PreviewData.products(),
|
products: if(real_products != [], do: real_products, else: PreviewData.products()),
|
||||||
categories: PreviewData.categories(),
|
categories: if(real_categories != [], do: real_categories, else: PreviewData.categories()),
|
||||||
cart_items: PreviewData.cart_items()
|
cart_items: PreviewData.cart_items()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +46,7 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
|> assign(:image_picker_search, "")
|
|> assign(:image_picker_search, "")
|
||||||
|> assign(:is_custom_page, !Page.system_slug?(slug))
|
|> assign(:is_custom_page, !Page.system_slug?(slug))
|
||||||
|> assign(:is_legal_page, LegalPages.legal_slug?(slug))
|
|> assign(:is_legal_page, LegalPages.legal_slug?(slug))
|
||||||
|
|> assign_settings_form(slug)
|
||||||
|> allow_upload(:image_picker_upload,
|
|> allow_upload(:image_picker_upload,
|
||||||
accept: ~w(.png .jpg .jpeg .webp .svg),
|
accept: ~w(.png .jpg .jpeg .webp .svg),
|
||||||
max_entries: 1,
|
max_entries: 1,
|
||||||
@ -462,6 +466,55 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
end
|
end
|
||||||
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 ───────────────────────────────────────────────────────
|
# ── Render ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -487,13 +540,16 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
/>
|
/>
|
||||||
{if @show_preview, do: "Edit", else: "Preview"}
|
{if @show_preview, do: "Edit", else: "Preview"}
|
||||||
</button>
|
</button>
|
||||||
<.link
|
<button
|
||||||
:if={@is_custom_page}
|
:if={@is_custom_page}
|
||||||
navigate={~p"/admin/pages/#{@slug}/settings"}
|
phx-click="toggle_settings"
|
||||||
class="admin-btn admin-btn-sm admin-btn-ghost"
|
class={[
|
||||||
|
"admin-btn admin-btn-sm admin-btn-ghost",
|
||||||
|
@show_settings && "admin-btn-active"
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<.icon name="hero-cog-6-tooth" class="size-4" /> Settings
|
<.icon name="hero-cog-6-tooth" class="size-4" /> Settings
|
||||||
</.link>
|
</button>
|
||||||
<button
|
<button
|
||||||
phx-click="undo"
|
phx-click="undo"
|
||||||
class={["admin-btn admin-btn-sm admin-btn-ghost", @history == [] && "opacity-30"]}
|
class={["admin-btn admin-btn-sm admin-btn-ghost", @history == [] && "opacity-30"]}
|
||||||
@ -554,6 +610,47 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div class="page-editor-container">
|
||||||
<%!-- Editor pane --%>
|
<%!-- Editor pane --%>
|
||||||
<div class={[
|
<div class={[
|
||||||
|
|||||||
@ -987,7 +987,7 @@ defmodule BerrypodWeb.Admin.PagesTest do
|
|||||||
test "shows settings button for custom pages", %{conn: conn} do
|
test "shows settings button for custom pages", %{conn: conn} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/admin/pages/size-guide")
|
{: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
|
end
|
||||||
|
|
||||||
test "does not show reset to defaults for custom pages", %{conn: conn} do
|
test "does not show reset to defaults for custom pages", %{conn: conn} do
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user