merge page settings into Page tab and remove Settings tab
All checks were successful
deploy / deploy (push) Successful in 3m37s
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:
parent
242fed0501
commit
dd7146cb41
@ -161,9 +161,11 @@ Restructure the 3-tab editor panel for better discoverability. Replace Settings
|
|||||||
| 10-14 | Header & footer navigation editors | 3h | done |
|
| 10-14 | Header & footer navigation editors | 3h | done |
|
||||||
| 15-16 | Footer content (about, copyright, newsletter toggle) | 1.25h | done |
|
| 15-16 | Footer content (about, copyright, newsletter toggle) | 1.25h | done |
|
||||||
| 17-18 | Move branding from Theme to Site | 1.5h | done |
|
| 17-18 | Move branding from Theme to Site | 1.5h | done |
|
||||||
| 19-20 | Merge page settings into Page tab, remove Settings tab | 1h | planned |
|
| 19-20 | Merge page settings into Page tab, remove Settings tab | 1h | done |
|
||||||
| 21-22 | Polish and testing | 2h | planned |
|
| 21-22 | Polish and testing | 2h | planned |
|
||||||
|
|
||||||
|
Custom page settings (title, slug, meta description, published, navigation options) now appear inline in the Page tab as a collapsible section. The separate Settings tab has been fully removed along with the SettingsEditor component.
|
||||||
|
|
||||||
Social links now support 40+ platforms (grouped by category), auto-detect platform from pasted URLs (including deep links like `tg://` and `spotify:`), normalize bare domains to https://, preserve custom protocols, and filter empty URLs from shop display.
|
Social links now support 40+ platforms (grouped by category), auto-detect platform from pasted URLs (including deep links like `tg://` and `spotify:`), normalize bare domains to https://, preserve custom protocols, and filter empty URLs from shop display.
|
||||||
|
|
||||||
Navigation editors support add/edit/delete/reorder for both header and footer nav items. Page picker dropdown allows linking to system pages (home, about, contact, etc.) or custom published pages. Changes preview immediately with live update in the shop layout. Removed legacy /admin/navigation page — nav editing is now exclusively through the Site tab with live preview.
|
Navigation editors support add/edit/delete/reorder for both header and footer nav items. Page picker dropdown allows linking to system pages (home, about, contact, etc.) or custom published pages. Changes preview immediately with live update in the shop layout. Removed legacy /admin/navigation page — nav editing is now exclusively through the Site tab with live preview.
|
||||||
|
|||||||
@ -2440,6 +2440,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Page settings section (inline in Page tab for custom pages) */
|
||||||
|
|
||||||
|
.page-settings-details {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: var(--admin-radius);
|
||||||
|
background: var(--admin-bg-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--admin-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .page-settings-chevron {
|
||||||
|
margin-left: auto;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-details[open] .page-settings-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0 0.75rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-slug-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-slug-prefix {
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
background: var(--admin-bg);
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-right: none;
|
||||||
|
border-radius: var(--admin-radius) 0 0 var(--admin-radius);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--admin-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-slug-input .admin-input {
|
||||||
|
border-radius: 0 var(--admin-radius) var(--admin-radius) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-checks {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-nav-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-field-inline {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-settings-field-sm {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Block list in editor */
|
/* Block list in editor */
|
||||||
|
|
||||||
.block-list {
|
.block-list {
|
||||||
@ -5133,72 +5226,6 @@
|
|||||||
/* 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; }
|
||||||
|
|||||||
@ -215,8 +215,8 @@ end
|
|||||||
| 16 | Footer: read new fields | 15 | 30m | done |
|
| 16 | Footer: read new fields | 15 | 30m | done |
|
||||||
| 17 | Move branding settings from Theme to Site | 4 | 1h | done |
|
| 17 | Move branding settings from Theme to Site | 4 | 1h | done |
|
||||||
| 18 | Theme tab: remove branding, polish remaining | 17 | 30m | done |
|
| 18 | Theme tab: remove branding, polish remaining | 17 | 30m | done |
|
||||||
| 19 | Merge page settings into Page tab | — | 45m | planned |
|
| 19 | Merge page settings into Page tab | — | 45m | done |
|
||||||
| 20 | Remove Settings tab | 19 | 15m | planned |
|
| 20 | Remove Settings tab | 19 | 15m | done |
|
||||||
| 21 | Polish: responsive, empty states, validation | 1-20 | 1.5h | planned |
|
| 21 | Polish: responsive, empty states, validation | 1-20 | 1.5h | planned |
|
||||||
| 22 | Testing: all page types, edge cases | 21 | 30m | planned |
|
| 22 | Testing: all page types, edge cases | 21 | 30m | planned |
|
||||||
|
|
||||||
@ -328,6 +328,25 @@ Moved branding settings from Theme tab to Site tab.
|
|||||||
- Accent, hover, sale colours
|
- Accent, hover, sale colours
|
||||||
- Customise accordion with typography, colours, layout, shape, products, product page groups
|
- Customise accordion with typography, colours, layout, shape, products, product page groups
|
||||||
|
|
||||||
|
### Completed: Page Settings Migration (Tasks 19-20)
|
||||||
|
|
||||||
|
Merged custom page settings inline into Page tab and removed Settings tab.
|
||||||
|
|
||||||
|
**What changed:**
|
||||||
|
- Custom pages show a collapsible "Page settings" section at the top of the Page tab
|
||||||
|
- Contains: title, URL slug, meta description, published toggle, show in nav toggle, nav label, nav position
|
||||||
|
- System pages, product pages, and collection pages no longer show settings (editing done elsewhere)
|
||||||
|
- Settings tab button was already removed from UI; dead code removed from page_renderer.ex
|
||||||
|
- Deleted `settings_editor.ex` component entirely
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
- `lib/berrypod_web/page_renderer.ex` - Added `page_settings_section` component inline in `editor_sheet_content`
|
||||||
|
- `lib/berrypod_web/components/shop_components.ex` - Removed SettingsEditor import
|
||||||
|
- `assets/css/admin/components.css` - Added `.page-settings-*` styles, removed old `.settings-*` styles
|
||||||
|
|
||||||
|
**Files deleted:**
|
||||||
|
- `lib/berrypod_web/components/shop_components/settings_editor.ex`
|
||||||
|
|
||||||
## UI Wireframes
|
## UI Wireframes
|
||||||
|
|
||||||
### Site tab layout
|
### Site tab layout
|
||||||
|
|||||||
@ -10,7 +10,6 @@ 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
|
||||||
@ -20,7 +19,6 @@ 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
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -120,9 +120,6 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
theme_editor_contrast_warning={Map.get(assigns, :theme_editor_contrast_warning, :ok)}
|
theme_editor_contrast_warning={Map.get(assigns, :theme_editor_contrast_warning, :ok)}
|
||||||
uploads={Map.get(assigns, :uploads)}
|
uploads={Map.get(assigns, :uploads)}
|
||||||
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_form={Map.get(assigns, :settings_form)}
|
||||||
settings_dirty={Map.get(assigns, :settings_dirty, false)}
|
settings_dirty={Map.get(assigns, :settings_dirty, false)}
|
||||||
settings_save_status={Map.get(assigns, :settings_save_status, :idle)}
|
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 :theme_editor_contrast_warning, :atom, default: :ok
|
||||||
attr :uploads, :map, default: nil
|
attr :uploads, :map, default: nil
|
||||||
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_form, :map, default: nil
|
||||||
attr :settings_dirty, :boolean, default: false
|
attr :settings_dirty, :boolean, default: false
|
||||||
attr :settings_save_status, :atom, default: :idle
|
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_images={@editor_image_picker_images}
|
||||||
editor_image_picker_search={@editor_image_picker_search}
|
editor_image_picker_search={@editor_image_picker_search}
|
||||||
editor_at_defaults={@editor_at_defaults}
|
editor_at_defaults={@editor_at_defaults}
|
||||||
|
settings_form={@settings_form}
|
||||||
|
settings_dirty={@settings_dirty}
|
||||||
|
settings_save_status={@settings_save_status}
|
||||||
/>
|
/>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@ -206,21 +203,6 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
"""
|
"""
|
||||||
end
|
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
|
defp editor_panel_content(%{editor_active_tab: :site} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<BerrypodWeb.ShopComponents.SiteEditor.site_editor
|
<BerrypodWeb.ShopComponents.SiteEditor.site_editor
|
||||||
@ -282,6 +264,9 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
attr :editor_image_picker_images, :list, default: []
|
attr :editor_image_picker_images, :list, default: []
|
||||||
attr :editor_image_picker_search, :string, default: ""
|
attr :editor_image_picker_search, :string, default: ""
|
||||||
attr :editor_at_defaults, :boolean, default: true
|
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
|
defp editor_sheet_content(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -320,6 +305,15 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
{if @editor_live_region_message, do: @editor_live_region_message}
|
{if @editor_live_region_message, do: @editor_live_region_message}
|
||||||
</div>
|
</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 --%>
|
<%!-- Block list --%>
|
||||||
<div class="block-list" role="list" aria-label="Page blocks">
|
<div class="block-list" role="list" aria-label="Page blocks">
|
||||||
<.block_card
|
<.block_card
|
||||||
@ -370,6 +364,141 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
"""
|
"""
|
||||||
end
|
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 ──────────────────────────────────────────────
|
# ── Block dispatch ──────────────────────────────────────────────
|
||||||
|
|
||||||
defp render_block(%{block: %{"type" => "hero"}} = assigns) do
|
defp render_block(%{block: %{"type" => "hero"}} = assigns) do
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user