merge page settings into Page tab and remove Settings tab
All checks were successful
deploy / deploy (push) Successful in 3m37s

Custom page settings (title, slug, meta, published, navigation) now
appear as a collapsible section at the top of the Page tab. The
separate Settings tab has been fully removed.

- Add page_settings_section component to page_renderer.ex
- Remove dead :settings case from editor_panel_content
- Delete settings_editor.ex component entirely
- Remove SettingsEditor import from shop_components.ex
- Add .page-settings-* CSS styles
- Clean up old .settings-* CSS styles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-28 23:54:04 +00:00
parent 242fed0501
commit dd7146cb41
6 changed files with 267 additions and 439 deletions

View File

@@ -10,7 +10,6 @@ defmodule BerrypodWeb.ShopComponents do
- `Product` — product cards, gallery, variant selector, hero sections
- `Content` — rich text, responsive images, contact form, reviews
- `ThemeEditor` — shared theme editor components for admin and on-site editing
- `SettingsEditor` — shared settings editor components for on-site editing
"""
defmacro __using__(_opts \\ []) do
@@ -20,7 +19,6 @@ defmodule BerrypodWeb.ShopComponents do
import BerrypodWeb.ShopComponents.Content
import BerrypodWeb.ShopComponents.Layout
import BerrypodWeb.ShopComponents.Product
import BerrypodWeb.ShopComponents.SettingsEditor
import BerrypodWeb.ShopComponents.ThemeEditor
end
end

View File

@@ -1,347 +0,0 @@
defmodule BerrypodWeb.ShopComponents.SettingsEditor do
@moduledoc """
Shared settings editor components used in the on-site editor panel.
Shows context-specific settings based on the current page type:
- Custom CMS pages: Title, slug, visibility, SEO meta, navigation
- System pages (home, about, etc.): Page info only (editing done in admin)
- Product pages: Read-only product info, link to admin
- Collection pages: Read-only collection info, link to admin
Components emit events with a configurable prefix:
- `settings_update_page` (phx-change/phx-submit)
The event prefix is controlled by `@event_prefix`:
- `"settings_"` (default) for on-site editor context
"""
use Phoenix.Component
import BerrypodWeb.CoreComponents, only: [icon: 1]
# ── Main Editor Component ────────────────────────────────────────────
@doc """
Renders the settings editor panel.
Content varies based on the page type (custom, system, product, collection).
"""
attr :page, :map, default: nil
attr :product, :map, default: nil
attr :collection_title, :string, default: nil
attr :live_action, :atom, default: nil
attr :settings_form, :any, default: nil
attr :settings_dirty, :boolean, default: false
attr :settings_save_status, :atom, default: :idle
attr :event_prefix, :string, default: "settings_"
def settings_editor(assigns) do
~H"""
<div class="editor-settings-content">
<%= cond do %>
<% @live_action == :product and @product -> %>
<.product_settings product={@product} />
<% @live_action == :collection and @collection_title -> %>
<.collection_settings collection_title={@collection_title} />
<% @page && @page[:type] == "custom" -> %>
<.custom_page_settings
page={@page}
form={@settings_form}
dirty={@settings_dirty}
save_status={@settings_save_status}
event_prefix={@event_prefix}
/>
<% @page -> %>
<.system_page_settings page={@page} />
<% true -> %>
<.no_settings_view />
<% end %>
</div>
"""
end
# ── Custom Page Settings ─────────────────────────────────────────────
attr :page, :map, required: true
attr :form, :any, default: nil
attr :dirty, :boolean, default: false
attr :save_status, :atom, default: :idle
attr :event_prefix, :string, default: "settings_"
defp custom_page_settings(assigns) do
# Use form values if available, otherwise fall back to page values
form = assigns.form || %{}
assigns =
assigns
|> assign(:form_title, form["title"] || assigns.page.title || "")
|> assign(:form_slug, form["slug"] || assigns.page.slug || "")
|> assign(:form_meta, form["meta_description"] || assigns.page.meta_description || "")
|> assign(:form_published, form_checked?(form, "published", assigns.page.published))
|> assign(:form_show_in_nav, form_checked?(form, "show_in_nav", assigns.page.show_in_nav))
|> assign(:form_nav_label, form["nav_label"] || assigns.page.nav_label || "")
|> assign(
:form_nav_position,
form["nav_position"] || to_string(assigns.page.nav_position || 0)
)
~H"""
<div class="settings-editor-form">
<form
id="settings-editor-form"
phx-change={@event_prefix <> "validate_page"}
phx-submit={@event_prefix <> "save_page"}
>
<div class="theme-section">
<label class="theme-section-label" for="settings-title">Title</label>
<input
type="text"
id="settings-title"
name="page[title]"
value={@form_title}
class="admin-input"
/>
</div>
<div class="theme-section">
<label class="theme-section-label" for="settings-slug">URL slug</label>
<div class="settings-slug-preview">
<span class="admin-text-tertiary">/</span>
<input
type="text"
id="settings-slug"
name="page[slug]"
value={@form_slug}
class="admin-input"
pattern="[a-z0-9-]+"
/>
</div>
</div>
<div class="theme-section">
<label class="theme-section-label" for="settings-meta">Meta description</label>
<textarea
id="settings-meta"
name="page[meta_description]"
rows="3"
class="admin-input admin-textarea"
placeholder="Brief description for search engines..."
>{@form_meta}</textarea>
<p class="admin-help-text">Shown in search results. Keep under 160 characters.</p>
</div>
<div class="theme-section">
<label class="admin-check-label">
<input
type="checkbox"
name="page[published]"
value="true"
checked={@form_published}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Published</span>
</label>
<p class="admin-help-text">Unpublished pages are only visible to admins.</p>
</div>
<div class="theme-section">
<label class="admin-check-label">
<input
type="checkbox"
name="page[show_in_nav]"
value="true"
checked={@form_show_in_nav}
class="admin-checkbox admin-checkbox-sm"
/>
<span class="theme-check-text">Show in navigation</span>
</label>
</div>
<div :if={@form_show_in_nav} class="theme-section settings-nav-options">
<div class="settings-nav-row">
<div class="settings-nav-field">
<label class="theme-section-label" for="settings-nav-label">Nav label</label>
<input
type="text"
id="settings-nav-label"
name="page[nav_label]"
value={@form_nav_label}
class="admin-input"
placeholder={@page.title}
/>
</div>
<div class="settings-nav-field settings-nav-field-sm">
<label class="theme-section-label" for="settings-nav-position">Position</label>
<input
type="number"
id="settings-nav-position"
name="page[nav_position]"
value={@form_nav_position}
class="admin-input"
min="0"
/>
</div>
</div>
</div>
<div class="theme-section settings-actions">
<button
type="submit"
class={[
"admin-btn admin-btn-sm",
@dirty && "admin-btn-primary",
!@dirty && "admin-btn-outline"
]}
disabled={!@dirty}
>
{if @save_status == :saving, do: "Saving...", else: "Save settings"}
</button>
<.save_status_indicator status={@save_status} />
</div>
</form>
<div class="theme-section settings-admin-link">
<a href={"/admin/pages/#{@page.slug}/settings"} class="admin-link">
<.icon name="hero-cog-6-tooth-mini" class="size-4" /> Full page settings
</a>
</div>
</div>
"""
end
# ── System Page Settings ─────────────────────────────────────────────
attr :page, :map, required: true
defp system_page_settings(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<label class="theme-section-label">Page</label>
<p class="admin-text-primary">{@page.title}</p>
</div>
<div class="theme-section">
<label class="theme-section-label">Type</label>
<p class="admin-text-secondary">System page</p>
</div>
<div class="theme-section">
<p class="admin-text-secondary">
This is a built-in page. Edit its content using the Page tab,
or <a href="/admin/pages" class="admin-link">manage pages in admin</a>.
</p>
</div>
</div>
"""
end
# ── Product Page Settings ────────────────────────────────────────────
attr :product, :map, required: true
defp product_settings(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<label class="theme-section-label">Product</label>
<p class="admin-text-primary">{@product.title}</p>
</div>
<div class="theme-section">
<label class="theme-section-label">Provider</label>
<p class="admin-text-secondary">{provider_label(@product)}</p>
</div>
<div class="theme-section">
<p class="admin-text-secondary">
Product details are synced from your print provider.
<a href={"/admin/products/#{@product.id}"} class="admin-link">View in admin</a>
to see pricing, variants, and images.
</p>
</div>
</div>
"""
end
defp provider_label(product) do
# provider_type is on the association, not the product itself
provider_type =
case product do
%{provider_connection: %{provider_type: type}} when is_binary(type) -> type
_ -> nil
end
case provider_type do
"printify" -> "Printify"
"printful" -> "Printful"
_ -> "Print provider"
end
end
# ── Collection Page Settings ─────────────────────────────────────────
attr :collection_title, :string, required: true
defp collection_settings(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<label class="theme-section-label">Collection</label>
<p class="admin-text-primary">{@collection_title}</p>
</div>
<div class="theme-section">
<p class="admin-text-secondary">
Collection pages show products filtered by category.
Edit the page layout using the Page tab.
</p>
</div>
</div>
"""
end
# ── No Settings View ─────────────────────────────────────────────────
defp no_settings_view(assigns) do
~H"""
<div class="settings-info-view">
<div class="theme-section">
<p class="admin-text-secondary">
This page doesn't have editable settings.
<a href="/admin/settings" class="admin-link">Shop settings</a>
can be changed in admin.
</p>
</div>
</div>
"""
end
# ── Helper Functions ─────────────────────────────────────────────────
# Check if a form checkbox should be checked
# Handles both form params (strings) and page values (booleans)
defp form_checked?(form, key, page_value) when is_map(form) do
case form[key] do
"true" -> true
"false" -> false
nil -> page_value == true
_ -> page_value == true
end
end
# ── Helper Components ────────────────────────────────────────────────
attr :status, :atom, required: true
defp save_status_indicator(assigns) do
~H"""
<span :if={@status == :saved} class="settings-save-indicator">
<.icon name="hero-check-mini" class="size-4" /> Saved
</span>
<span :if={@status == :error} class="settings-save-indicator settings-save-error">
<.icon name="hero-exclamation-circle-mini" class="size-4" /> Error
</span>
"""
end
end

View File

@@ -120,9 +120,6 @@ defmodule BerrypodWeb.PageRenderer do
theme_editor_contrast_warning={Map.get(assigns, :theme_editor_contrast_warning, :ok)}
uploads={Map.get(assigns, :uploads)}
site_name={Map.get(assigns, :site_name, "")}
product={assigns[:product]}
collection_title={assigns[:collection_title]}
live_action={assigns[:live_action]}
settings_form={Map.get(assigns, :settings_form)}
settings_dirty={Map.get(assigns, :settings_dirty, false)}
settings_save_status={Map.get(assigns, :settings_save_status, :idle)}
@@ -159,9 +156,6 @@ defmodule BerrypodWeb.PageRenderer do
attr :theme_editor_contrast_warning, :atom, default: :ok
attr :uploads, :map, default: nil
attr :site_name, :string, default: ""
attr :product, :map, default: nil
attr :collection_title, :string, default: nil
attr :live_action, :atom, default: nil
attr :settings_form, :map, default: nil
attr :settings_dirty, :boolean, default: false
attr :settings_save_status, :atom, default: :idle
@@ -185,6 +179,9 @@ defmodule BerrypodWeb.PageRenderer do
editor_image_picker_images={@editor_image_picker_images}
editor_image_picker_search={@editor_image_picker_search}
editor_at_defaults={@editor_at_defaults}
settings_form={@settings_form}
settings_dirty={@settings_dirty}
settings_save_status={@settings_save_status}
/>
"""
end
@@ -206,21 +203,6 @@ defmodule BerrypodWeb.PageRenderer do
"""
end
defp editor_panel_content(%{editor_active_tab: :settings} = assigns) do
# Legacy settings tab - will be removed once page settings are merged into Page tab
~H"""
<BerrypodWeb.ShopComponents.SettingsEditor.settings_editor
page={@page}
product={@product}
collection_title={@collection_title}
live_action={@live_action}
settings_form={@settings_form}
settings_dirty={@settings_dirty}
settings_save_status={@settings_save_status}
/>
"""
end
defp editor_panel_content(%{editor_active_tab: :site} = assigns) do
~H"""
<BerrypodWeb.ShopComponents.SiteEditor.site_editor
@@ -282,6 +264,9 @@ defmodule BerrypodWeb.PageRenderer do
attr :editor_image_picker_images, :list, default: []
attr :editor_image_picker_search, :string, default: ""
attr :editor_at_defaults, :boolean, default: true
attr :settings_form, :map, default: nil
attr :settings_dirty, :boolean, default: false
attr :settings_save_status, :atom, default: :idle
defp editor_sheet_content(assigns) do
~H"""
@@ -320,6 +305,15 @@ defmodule BerrypodWeb.PageRenderer do
{if @editor_live_region_message, do: @editor_live_region_message}
</div>
<%!-- Page settings for custom pages --%>
<.page_settings_section
:if={@page[:type] == "custom"}
page={@page}
form={@settings_form}
dirty={@settings_dirty}
save_status={@settings_save_status}
/>
<%!-- Block list --%>
<div class="block-list" role="list" aria-label="Page blocks">
<.block_card
@@ -370,6 +364,141 @@ defmodule BerrypodWeb.PageRenderer do
"""
end
# Page settings section for custom pages (collapsible)
attr :page, :map, required: true
attr :form, :map, default: nil
attr :dirty, :boolean, default: false
attr :save_status, :atom, default: :idle
defp page_settings_section(assigns) do
form = assigns.form || %{}
assigns =
assigns
|> assign(:form_title, form["title"] || assigns.page.title || "")
|> assign(:form_slug, form["slug"] || assigns.page.slug || "")
|> assign(:form_meta, form["meta_description"] || assigns.page.meta_description || "")
|> assign(:form_published, form_checked?(form, "published", assigns.page.published))
|> assign(:form_show_in_nav, form_checked?(form, "show_in_nav", assigns.page.show_in_nav))
|> assign(:form_nav_label, form["nav_label"] || assigns.page.nav_label || "")
|> assign(
:form_nav_position,
form["nav_position"] || to_string(assigns.page.nav_position || 0)
)
~H"""
<details class="page-settings-details" open>
<summary class="page-settings-summary">
<.icon name="hero-cog-6-tooth-mini" class="size-4" />
<span>Page settings</span>
<.icon name="hero-chevron-down-mini" class="size-4 page-settings-chevron" />
</summary>
<form
id="page-settings-form"
class="page-settings-form"
phx-change="settings_validate_page"
phx-submit="settings_save_page"
>
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-title">Title</label>
<input
type="text"
id="page-settings-title"
name="page[title]"
value={@form_title}
class="admin-input"
/>
</div>
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-slug">URL slug</label>
<div class="page-settings-slug-input">
<span class="page-settings-slug-prefix">/</span>
<input
type="text"
id="page-settings-slug"
name="page[slug]"
value={@form_slug}
class="admin-input"
pattern="[a-z0-9-]+"
/>
</div>
</div>
<div class="page-settings-field">
<label class="page-settings-label" for="page-settings-meta">Meta description</label>
<textarea
id="page-settings-meta"
name="page[meta_description]"
rows="2"
class="admin-input admin-textarea"
placeholder="Brief description for search engines..."
>{@form_meta}</textarea>
</div>
<div class="page-settings-checks">
<label class="admin-check-label">
<input
type="checkbox"
name="page[published]"
value="true"
checked={@form_published}
class="admin-checkbox admin-checkbox-sm"
/>
<span>Published</span>
</label>
<label class="admin-check-label">
<input
type="checkbox"
name="page[show_in_nav]"
value="true"
checked={@form_show_in_nav}
class="admin-checkbox admin-checkbox-sm"
/>
<span>Show in navigation</span>
</label>
</div>
<div :if={@form_show_in_nav} class="page-settings-nav-options">
<div class="page-settings-field page-settings-field-inline">
<label class="page-settings-label" for="page-settings-nav-label">Nav label</label>
<input
type="text"
id="page-settings-nav-label"
name="page[nav_label]"
value={@form_nav_label}
class="admin-input"
placeholder={@page.title}
/>
</div>
<div class="page-settings-field page-settings-field-sm">
<label class="page-settings-label" for="page-settings-nav-position">Position</label>
<input
type="number"
id="page-settings-nav-position"
name="page[nav_position]"
value={@form_nav_position}
class="admin-input"
min="0"
/>
</div>
</div>
</form>
</details>
"""
end
defp form_checked?(form, key, page_value) when is_map(form) do
case form[key] do
"true" -> true
"false" -> false
nil -> page_value == true
_ -> page_value == true
end
end
# ── Block dispatch ──────────────────────────────────────────────
defp render_block(%{block: %{"type" => "hero"}} = assigns) do