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:
parent
bb5d220079
commit
f7891188e0
@ -89,8 +89,8 @@ Extend the existing page editor (PageEditorHook + editor_sheet) to include theme
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | Add theme editing state to PageEditorHook | 2h | done |
|
| 1 | Add theme editing state to PageEditorHook | 2h | done |
|
||||||
| 2 | Add 3-tab UI to editor panel (Page/Theme/Settings) | 2h | done |
|
| 2 | Add 3-tab UI to editor panel (Page/Theme/Settings) | 2h | done |
|
||||||
| 3 | Extract theme editor into reusable component | 3h | planned |
|
| 3 | Extract theme editor into reusable component | 3h | done |
|
||||||
| 3b | Create settings editor component | 2h | planned |
|
| 3b | Create settings editor component | 2h | done |
|
||||||
| 4 | Image upload handling in hook context | 2h | planned |
|
| 4 | Image upload handling in hook context | 2h | planned |
|
||||||
| 5 | URL-based mode activation (?edit=theme) | 1h | planned |
|
| 5 | URL-based mode activation (?edit=theme) | 1h | planned |
|
||||||
| 6 | Admin routing redirect | 30m | planned |
|
| 6 | Admin routing redirect | 30m | planned |
|
||||||
|
|||||||
@ -4600,6 +4600,72 @@
|
|||||||
/* Last group in a customise section (no border, tighter margin than theme-group) */
|
/* Last group in a customise section (no border, tighter margin than theme-group) */
|
||||||
.theme-group-flush { margin-bottom: 1rem; }
|
.theme-group-flush { margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
/* ── Settings editor (on-site editor Settings tab) ── */
|
||||||
|
|
||||||
|
.settings-editor-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-slug-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-slug-preview .admin-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav-options {
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 2px solid var(--admin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav-field-sm {
|
||||||
|
flex: 0 0 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-save-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--admin-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-save-error {
|
||||||
|
color: var(--admin-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-admin-link {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
border-top: 1px solid var(--admin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-info-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Content width containers ── */
|
/* ── Content width containers ── */
|
||||||
|
|
||||||
.admin-content-narrow { max-width: 32rem; }
|
.admin-content-narrow { max-width: 32rem; }
|
||||||
|
|||||||
@ -10,6 +10,7 @@ defmodule BerrypodWeb.ShopComponents do
|
|||||||
- `Product` — product cards, gallery, variant selector, hero sections
|
- `Product` — product cards, gallery, variant selector, hero sections
|
||||||
- `Content` — rich text, responsive images, contact form, reviews
|
- `Content` — rich text, responsive images, contact form, reviews
|
||||||
- `ThemeEditor` — shared theme editor components for admin and on-site editing
|
- `ThemeEditor` — shared theme editor components for admin and on-site editing
|
||||||
|
- `SettingsEditor` — shared settings editor components for on-site editing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
defmacro __using__(_opts \\ []) do
|
defmacro __using__(_opts \\ []) do
|
||||||
@ -19,6 +20,7 @@ defmodule BerrypodWeb.ShopComponents do
|
|||||||
import BerrypodWeb.ShopComponents.Content
|
import BerrypodWeb.ShopComponents.Content
|
||||||
import BerrypodWeb.ShopComponents.Layout
|
import BerrypodWeb.ShopComponents.Layout
|
||||||
import BerrypodWeb.ShopComponents.Product
|
import BerrypodWeb.ShopComponents.Product
|
||||||
|
import BerrypodWeb.ShopComponents.SettingsEditor
|
||||||
import BerrypodWeb.ShopComponents.ThemeEditor
|
import BerrypodWeb.ShopComponents.ThemeEditor
|
||||||
end
|
end
|
||||||
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
|
||||||
@ -61,6 +61,9 @@ defmodule BerrypodWeb.PageEditorHook do
|
|||||||
|> assign(:theme_editor_contrast_warning, :ok)
|
|> assign(:theme_editor_contrast_warning, :ok)
|
||||||
|> assign(:theme_editor_customise_open, false)
|
|> assign(:theme_editor_customise_open, false)
|
||||||
|> assign(:theme_editor_presets, Presets.all_with_descriptions())
|
|> assign(:theme_editor_presets, Presets.all_with_descriptions())
|
||||||
|
# Settings editing state
|
||||||
|
|> assign(:settings_dirty, false)
|
||||||
|
|> assign(:settings_save_status, :idle)
|
||||||
|> attach_hook(:editor_params, :handle_params, &handle_editor_params/3)
|
|> attach_hook(:editor_params, :handle_params, &handle_editor_params/3)
|
||||||
|> attach_hook(:editor_events, :handle_event, &handle_editor_event/3)
|
|> attach_hook(:editor_events, :handle_event, &handle_editor_event/3)
|
||||||
|> attach_hook(:editor_info, :handle_info, &handle_editor_info/2)
|
|> attach_hook(:editor_info, :handle_info, &handle_editor_info/2)
|
||||||
@ -212,6 +215,16 @@ defmodule BerrypodWeb.PageEditorHook do
|
|||||||
load_theme_state(socket)
|
load_theme_state(socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Initialize settings form from current page if not already set
|
||||||
|
# Only custom pages have editable settings (meta_description, published, etc.)
|
||||||
|
socket =
|
||||||
|
if is_nil(socket.assigns[:settings_form]) && socket.assigns[:page] &&
|
||||||
|
socket.assigns.page[:type] == "custom" do
|
||||||
|
init_settings_form(socket)
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
assign(socket, :editor_active_tab, :settings)
|
assign(socket, :editor_active_tab, :settings)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -251,6 +264,15 @@ defmodule BerrypodWeb.PageEditorHook do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Settings editing events (custom page settings)
|
||||||
|
defp handle_editor_event("settings_" <> action, params, socket) do
|
||||||
|
if socket.assigns.is_admin do
|
||||||
|
handle_settings_action(action, params, socket)
|
||||||
|
else
|
||||||
|
{:cont, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_editor_event(_event, _params, socket), do: {:cont, socket}
|
defp handle_editor_event(_event, _params, socket), do: {:cont, socket}
|
||||||
|
|
||||||
# ── Block manipulation actions ───────────────────────────────────
|
# ── Block manipulation actions ───────────────────────────────────
|
||||||
@ -721,6 +743,106 @@ defmodule BerrypodWeb.PageEditorHook do
|
|||||||
# Catch-all for unknown theme actions
|
# Catch-all for unknown theme actions
|
||||||
defp handle_theme_action(_action, _params, socket), do: {:halt, socket}
|
defp handle_theme_action(_action, _params, socket), do: {:halt, socket}
|
||||||
|
|
||||||
|
# ── Settings editing actions (custom page settings) ────────────────
|
||||||
|
|
||||||
|
# Validate page settings form (live as-you-type)
|
||||||
|
defp handle_settings_action("validate_page", %{"page" => params}, socket) do
|
||||||
|
page = socket.assigns.page
|
||||||
|
|
||||||
|
# Only allow editing custom pages
|
||||||
|
if page && page.type == "custom" do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:settings_form, params)
|
||||||
|
|> assign(:settings_dirty, has_settings_changed?(page, params))
|
||||||
|
|> assign(:settings_save_status, :idle)
|
||||||
|
|
||||||
|
{:halt, socket}
|
||||||
|
else
|
||||||
|
{:halt, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Save page settings
|
||||||
|
defp handle_settings_action("save_page", %{"page" => params}, socket) do
|
||||||
|
page = socket.assigns.page
|
||||||
|
|
||||||
|
# Only allow editing custom pages
|
||||||
|
if page && page.type == "custom" do
|
||||||
|
# Normalize checkbox fields (unchecked checkboxes aren't sent)
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.put_new("published", "false")
|
||||||
|
|> Map.put_new("show_in_nav", "false")
|
||||||
|
|
||||||
|
old_slug = page.slug
|
||||||
|
|
||||||
|
# Fetch the Page struct from DB (assigns.page may be a map from cache)
|
||||||
|
page_struct = Pages.get_page_struct(page.slug)
|
||||||
|
|
||||||
|
case Pages.update_custom_page(page_struct, params) do
|
||||||
|
{:ok, updated_page} ->
|
||||||
|
# Reinitialize form from saved page
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:page, updated_page)
|
||||||
|
|> assign(:settings_form, nil)
|
||||||
|
|> assign(:settings_dirty, false)
|
||||||
|
|> assign(:settings_save_status, :saved)
|
||||||
|
|
||||||
|
# Reinit form with new page values
|
||||||
|
socket = init_settings_form(socket)
|
||||||
|
|
||||||
|
# If slug changed, redirect to new URL
|
||||||
|
socket =
|
||||||
|
if updated_page.slug != old_slug do
|
||||||
|
push_navigate(socket, to: "/#{updated_page.slug}")
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
{:halt, socket}
|
||||||
|
|
||||||
|
{:error, _changeset} ->
|
||||||
|
socket = assign(socket, :settings_save_status, :error)
|
||||||
|
{:halt, socket}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:halt, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Catch-all for unknown settings actions
|
||||||
|
defp handle_settings_action(_action, _params, socket), do: {:halt, socket}
|
||||||
|
|
||||||
|
# Check if settings have changed from current page values
|
||||||
|
defp has_settings_changed?(page, params) do
|
||||||
|
page.title != (params["title"] || "") or
|
||||||
|
page.slug != (params["slug"] || "") or
|
||||||
|
(page.meta_description || "") != (params["meta_description"] || "") or
|
||||||
|
to_string(page.published) != (params["published"] || "false") or
|
||||||
|
to_string(page.show_in_nav) != (params["show_in_nav"] || "false") or
|
||||||
|
(page.nav_label || "") != (params["nav_label"] || "") or
|
||||||
|
to_string(page.nav_position || 0) != (params["nav_position"] || "0")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initialize settings form from page values
|
||||||
|
defp init_settings_form(socket) do
|
||||||
|
page = socket.assigns.page
|
||||||
|
|
||||||
|
form = %{
|
||||||
|
"title" => page.title || "",
|
||||||
|
"slug" => page.slug || "",
|
||||||
|
"meta_description" => page.meta_description || "",
|
||||||
|
"published" => to_string(page.published),
|
||||||
|
"show_in_nav" => to_string(page.show_in_nav),
|
||||||
|
"nav_label" => page.nav_label || "",
|
||||||
|
"nav_position" => to_string(page.nav_position || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(socket, :settings_form, form)
|
||||||
|
end
|
||||||
|
|
||||||
# Helper to update a theme setting and regenerate CSS
|
# Helper to update a theme setting and regenerate CSS
|
||||||
defp update_theme_setting(socket, attrs, field) do
|
defp update_theme_setting(socket, attrs, field) do
|
||||||
case Settings.update_theme_settings(attrs) do
|
case Settings.update_theme_settings(attrs) do
|
||||||
|
|||||||
@ -112,6 +112,12 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
theme_editor_presets={Map.get(assigns, :theme_editor_presets, [])}
|
theme_editor_presets={Map.get(assigns, :theme_editor_presets, [])}
|
||||||
theme_editor_customise_open={Map.get(assigns, :theme_editor_customise_open, false)}
|
theme_editor_customise_open={Map.get(assigns, :theme_editor_customise_open, false)}
|
||||||
site_name={Map.get(assigns, :site_name, "")}
|
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)}
|
||||||
/>
|
/>
|
||||||
</.editor_sheet>
|
</.editor_sheet>
|
||||||
"""
|
"""
|
||||||
@ -138,6 +144,12 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
attr :theme_editor_presets, :list, default: []
|
attr :theme_editor_presets, :list, default: []
|
||||||
attr :theme_editor_customise_open, :boolean, default: false
|
attr :theme_editor_customise_open, :boolean, default: false
|
||||||
attr :site_name, :string, default: ""
|
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
|
||||||
|
|
||||||
defp editor_panel_content(%{editor_active_tab: :page} = assigns) do
|
defp editor_panel_content(%{editor_active_tab: :page} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -174,7 +186,15 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
|
|
||||||
defp editor_panel_content(%{editor_active_tab: :settings} = assigns) do
|
defp editor_panel_content(%{editor_active_tab: :settings} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.settings_editor_content page={@page} site_name={@site_name} />
|
<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
|
end
|
||||||
|
|
||||||
@ -198,37 +218,6 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
# Settings editor content - shows page/shop settings
|
|
||||||
attr :page, :map, default: nil
|
|
||||||
attr :site_name, :string, default: ""
|
|
||||||
|
|
||||||
defp settings_editor_content(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="editor-settings-content">
|
|
||||||
<%= if @page do %>
|
|
||||||
<div class="theme-section">
|
|
||||||
<label class="theme-section-label">Page</label>
|
|
||||||
<p class="admin-text-secondary">{@page.title}</p>
|
|
||||||
</div>
|
|
||||||
<div class="theme-section">
|
|
||||||
<p class="admin-text-secondary">
|
|
||||||
Page settings like SEO, visibility, and slug editing coming soon.
|
|
||||||
For now, <a href="/admin/pages" class="admin-link">manage pages in admin</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<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>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Editor sheet content - the block list and editing controls
|
# Editor sheet content - the block list and editing controls
|
||||||
attr :page, :map, default: nil
|
attr :page, :map, default: nil
|
||||||
attr :editing_blocks, :list, default: nil
|
attr :editing_blocks, :list, default: nil
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user