add settings editor component for unified on-site editing
All checks were successful
deploy / deploy (push) Successful in 4m13s
All checks were successful
deploy / deploy (push) Successful in 4m13s
Phase 3b of unified editing mode. The Settings tab now shows context-specific forms: custom pages get editable title, slug, meta, visibility and nav options; system pages get read-only info with links to admin; product/collection pages show provider info. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ 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
|
||||
@@ -19,6 +20,7 @@ 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
|
||||
|
||||
340
lib/berrypod_web/components/shop_components/settings_editor.ex
Normal file
340
lib/berrypod_web/components/shop_components/settings_editor.ex
Normal file
@@ -0,0 +1,340 @@
|
||||
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
|
||||
case product.provider_type do
|
||||
"printify" -> "Printify"
|
||||
"printful" -> "Printful"
|
||||
_ -> "Unknown"
|
||||
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
|
||||
Reference in New Issue
Block a user