move branding settings from Theme tab to Site tab
Some checks failed
deploy / deploy (push) Failing after 10m13s
Some checks failed
deploy / deploy (push) Failing after 10m13s
- Add branding_editor component to site_editor.ex with: - Shop name input - Show shop name / Show logo toggles - Logo upload (size slider, SVG recolor, color picker) - Header background toggle and upload (zoom, position sliders) - Add site_ prefixed event handlers in page_editor_hook.ex: - site_update_branding, site_toggle_branding, site_update_color - site_remove_logo and site_remove_header (delegate to theme handlers) - Remove branding sections from theme_editor.ex: - Deleted shop_name_input, branding_section, logo/header upload sections - Theme tab now shows only: preset grid, accent colours, customise accordion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7c07805df8
commit
242fed0501
@ -160,13 +160,13 @@ Restructure the 3-tab editor panel for better discoverability. Replace Settings
|
||||
| 8-9 | Social links editor (CRUD, reorder, platform detection, URL normalization) | 2h | done |
|
||||
| 10-14 | Header & footer navigation editors | 3h | done |
|
||||
| 15-16 | Footer content (about, copyright, newsletter toggle) | 1.25h | done |
|
||||
| 17-18 | Move branding from Theme to Site | 1.5h | planned |
|
||||
| 17-18 | Move branding from Theme to Site | 1.5h | done |
|
||||
| 19-20 | Merge page settings into Page tab, remove Settings tab | 1h | planned |
|
||||
| 21-22 | Polish and testing | 2h | planned |
|
||||
|
||||
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. Items persist immediately on change.
|
||||
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.
|
||||
|
||||
### Unified editor session ([plan](docs/plans/unified-editor-session.md)) — Complete
|
||||
|
||||
|
||||
@ -213,8 +213,8 @@ end
|
||||
| 14 | Footer nav: read from database | 13 | 30m | done |
|
||||
| 15 | Footer content: about, copyright, newsletter toggle | 4 | 45m | done |
|
||||
| 16 | Footer: read new fields | 15 | 30m | done |
|
||||
| 17 | Move branding settings from Theme to Site | 4 | 1h | planned |
|
||||
| 18 | Theme tab: remove branding, polish remaining | 17 | 30m | planned |
|
||||
| 17 | Move branding settings from Theme to Site | 4 | 1h | done |
|
||||
| 18 | Theme tab: remove branding, polish remaining | 17 | 30m | done |
|
||||
| 19 | Merge page settings into Page tab | — | 45m | planned |
|
||||
| 20 | Remove Settings tab | 19 | 15m | planned |
|
||||
| 21 | Polish: responsive, empty states, validation | 1-20 | 1.5h | planned |
|
||||
@ -300,6 +300,34 @@ Header and footer navigation items are managed through a unified `nav_editor` co
|
||||
- `lib/berrypod_web/page_editor_hook.ex` - Event handlers, sync functions, dirty state
|
||||
- `assets/css/admin/components.css` - Styles for nav editor
|
||||
|
||||
### Completed: Branding Migration (Tasks 17-18)
|
||||
|
||||
Moved branding settings from Theme tab to Site tab.
|
||||
|
||||
**What moved:**
|
||||
- Shop name input
|
||||
- Show shop name / Show logo toggles
|
||||
- Logo upload (file picker, size slider, recolor toggle, color picker)
|
||||
- Header background toggle
|
||||
- Header image upload (file picker, zoom slider, position sliders)
|
||||
|
||||
**Event routing:**
|
||||
- Site tab branding events use `site_` prefix (e.g., `site_update_branding`, `site_toggle_branding`)
|
||||
- These route to the same underlying theme state (`theme_editor_settings`, `theme_editor_logo_image`, etc.)
|
||||
- Logo/header uploads still use `theme_logo_upload` and `theme_header_upload` (shared with theme save flow)
|
||||
- `site_remove_logo` and `site_remove_header` delegate to theme action handlers
|
||||
|
||||
**Files modified:**
|
||||
- `lib/berrypod_web/page_renderer.ex` - Pass branding props to site_editor
|
||||
- `lib/berrypod_web/components/shop_components/site_editor.ex` - Add `branding_editor` component with logo/header upload sections
|
||||
- `lib/berrypod_web/page_editor_hook.ex` - Add `site_update_branding`, `site_toggle_branding`, `site_update_color`, `site_remove_logo`, `site_remove_header` handlers
|
||||
- `lib/berrypod_web/components/shop_components/theme_editor.ex` - Remove `shop_name_input`, `branding_section`, logo/header upload sections
|
||||
|
||||
**Theme tab now contains:**
|
||||
- Preset grid (8 themes)
|
||||
- Accent, hover, sale colours
|
||||
- Customise accordion with typography, colours, layout, shape, products, product page groups
|
||||
|
||||
## UI Wireframes
|
||||
|
||||
### Site tab layout
|
||||
|
||||
@ -24,10 +24,18 @@ defmodule BerrypodWeb.ShopComponents.SiteEditor do
|
||||
Expects:
|
||||
- site_state: SiteEditorState struct with all site tab state
|
||||
- site_nav_pages: list of pages for the nav picker
|
||||
- Branding-related props passed from theme state
|
||||
"""
|
||||
attr :site_state, :any, required: true
|
||||
attr :site_nav_pages, :list, default: []
|
||||
attr :event_prefix, :string, default: "site_"
|
||||
attr :site_name, :string, default: ""
|
||||
attr :theme_settings, :map, default: nil
|
||||
attr :logo_image, :map, default: nil
|
||||
attr :header_image, :map, default: nil
|
||||
attr :icon_image, :map, default: nil
|
||||
attr :contrast_warning, :atom, default: :ok
|
||||
attr :uploads, :map, default: nil
|
||||
|
||||
def site_editor(%{site_state: nil} = assigns) do
|
||||
~H"""
|
||||
@ -60,7 +68,16 @@ defmodule BerrypodWeb.ShopComponents.SiteEditor do
|
||||
~H"""
|
||||
<div class="editor-site-content">
|
||||
<.site_section title="Branding" icon="hero-sparkles" open={true}>
|
||||
<.branding_placeholder />
|
||||
<.branding_editor
|
||||
site_name={@site_name}
|
||||
theme_settings={@theme_settings}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
icon_image={@icon_image}
|
||||
contrast_warning={@contrast_warning}
|
||||
uploads={@uploads}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
</.site_section>
|
||||
|
||||
<.site_section title="Announcement bar" icon="hero-megaphone">
|
||||
@ -128,19 +145,356 @@ defmodule BerrypodWeb.ShopComponents.SiteEditor do
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Branding Section (placeholder) ─────────────────────────────────
|
||||
# ── Branding Editor ─────────────────────────────────────────────────
|
||||
|
||||
defp branding_placeholder(assigns) do
|
||||
attr :site_name, :string, default: ""
|
||||
attr :theme_settings, :map, default: nil
|
||||
attr :logo_image, :map, default: nil
|
||||
attr :header_image, :map, default: nil
|
||||
attr :icon_image, :map, default: nil
|
||||
attr :contrast_warning, :atom, default: :ok
|
||||
attr :uploads, :map, default: nil
|
||||
attr :event_prefix, :string, default: "site_"
|
||||
|
||||
defp branding_editor(%{theme_settings: nil} = assigns) do
|
||||
~H"""
|
||||
<div class="site-editor-placeholder">
|
||||
<p class="admin-text-secondary">
|
||||
Branding settings (shop name, logo, favicon) will be moved here from the Theme tab.
|
||||
</p>
|
||||
<p class="admin-help-text">Coming soon in the next phase.</p>
|
||||
<p class="admin-text-secondary">Loading branding settings...</p>
|
||||
"""
|
||||
end
|
||||
|
||||
defp branding_editor(assigns) do
|
||||
~H"""
|
||||
<div class="site-editor-form">
|
||||
<%!-- Shop name --%>
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Shop name</label>
|
||||
<form phx-change={@event_prefix <> "update_branding"} phx-value-field="site_name">
|
||||
<input
|
||||
type="text"
|
||||
name="site_name"
|
||||
value={@site_name}
|
||||
placeholder="Your shop name"
|
||||
class="admin-input"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<%!-- Display toggles --%>
|
||||
<div class="admin-stack admin-stack-sm theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.show_site_name}
|
||||
phx-click={@event_prefix <> "toggle_branding"}
|
||||
phx-value-field="show_site_name"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Show shop name</span>
|
||||
</label>
|
||||
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.show_logo}
|
||||
phx-click={@event_prefix <> "toggle_branding"}
|
||||
phx-value-field="show_logo"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Show logo</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<%!-- Logo upload section --%>
|
||||
<%= if @theme_settings.show_logo && @uploads do %>
|
||||
<.logo_upload_section
|
||||
uploads={@uploads}
|
||||
logo_image={@logo_image}
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<%!-- Header background toggle --%>
|
||||
<label class="admin-check-label theme-field">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.header_background_enabled}
|
||||
phx-click={@event_prefix <> "update_branding"}
|
||||
phx-value-field="header_background_enabled"
|
||||
phx-value-setting_value={
|
||||
if @theme_settings.header_background_enabled, do: "false", else: "true"
|
||||
}
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Header background image</span>
|
||||
</label>
|
||||
|
||||
<%!-- Header upload section --%>
|
||||
<%= if @theme_settings.header_background_enabled && @uploads do %>
|
||||
<.header_upload_section
|
||||
uploads={@uploads}
|
||||
header_image={@header_image}
|
||||
theme_settings={@theme_settings}
|
||||
contrast_warning={@contrast_warning}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Logo upload sub-section
|
||||
attr :uploads, :map, required: true
|
||||
attr :logo_image, :map, default: nil
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, default: ""
|
||||
attr :event_prefix, :string, default: "site_"
|
||||
|
||||
defp logo_upload_section(assigns) do
|
||||
~H"""
|
||||
<div class="theme-subsection">
|
||||
<span class="theme-slider-label theme-block-label">Upload logo (SVG or PNG)</span>
|
||||
<div class="admin-row admin-row-lg">
|
||||
<form phx-change="noop" phx-submit="noop" class="admin-fill">
|
||||
<label class="theme-upload-label theme-upload-label-compact">
|
||||
<span>Choose file...</span>
|
||||
<.live_file_input upload={@uploads.theme_logo_upload} class="hidden" />
|
||||
</label>
|
||||
</form>
|
||||
<%= if @logo_image do %>
|
||||
<div class="theme-thumb theme-thumb-logo theme-thumb-compact">
|
||||
<img src={"/image_cache/#{@logo_image.id}.webp"} alt={@site_name} />
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "remove_logo"}
|
||||
class="theme-remove-btn"
|
||||
title="Remove logo"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= for entry <- @uploads.theme_logo_upload.entries do %>
|
||||
<.upload_progress entry={entry} upload_name="theme_logo_upload" />
|
||||
<%= for err <- upload_errors(@uploads.theme_logo_upload, entry) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= for err <- upload_errors(@uploads.theme_logo_upload) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
|
||||
<%= if @logo_image do %>
|
||||
<form
|
||||
phx-change={@event_prefix <> "update_branding"}
|
||||
phx-value-field="logo_size"
|
||||
class="theme-subfield"
|
||||
>
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Logo size</span>
|
||||
<span class="theme-slider-value">{@theme_settings.logo_size}px</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="24"
|
||||
max="120"
|
||||
value={@theme_settings.logo_size}
|
||||
name="logo_size"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<%= if @logo_image.is_svg do %>
|
||||
<div class="theme-subfield">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.logo_recolor}
|
||||
phx-click={@event_prefix <> "update_branding"}
|
||||
phx-value-field="logo_recolor"
|
||||
phx-value-setting_value={if @theme_settings.logo_recolor, do: "false", else: "true"}
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Recolour logo</span>
|
||||
</label>
|
||||
|
||||
<%= if @theme_settings.logo_recolor do %>
|
||||
<form
|
||||
id="logo-color-form-site"
|
||||
phx-change={@event_prefix <> "update_color"}
|
||||
phx-value-field="logo_color"
|
||||
phx-hook="ColorSync"
|
||||
class="theme-color-row theme-subfield-sm"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
name="value"
|
||||
value={@theme_settings.logo_color}
|
||||
class="theme-color-swatch theme-color-swatch-sm"
|
||||
/>
|
||||
<span class="theme-color-value">{@theme_settings.logo_color}</span>
|
||||
</form>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Header upload sub-section
|
||||
attr :uploads, :map, required: true
|
||||
attr :header_image, :map, default: nil
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :contrast_warning, :atom, default: :ok
|
||||
attr :event_prefix, :string, default: "site_"
|
||||
|
||||
defp header_upload_section(assigns) do
|
||||
~H"""
|
||||
<div class="theme-subsection">
|
||||
<span class="theme-slider-label theme-block-label">Upload header image</span>
|
||||
<form phx-change="noop" phx-submit="noop">
|
||||
<label class="theme-upload-label theme-upload-label-compact">
|
||||
<span>Choose file...</span>
|
||||
<.live_file_input upload={@uploads.theme_header_upload} class="hidden" />
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<%= if @header_image do %>
|
||||
<div class="theme-thumb theme-thumb-cover theme-thumb-header theme-thumb-compact">
|
||||
<img src={"/image_cache/#{@header_image.id}.webp"} alt="" />
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "remove_header"}
|
||||
class="theme-remove-btn"
|
||||
title="Remove header background"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%= if @contrast_warning != :ok do %>
|
||||
<div class="theme-contrast-warning theme-contrast-warning-compact">
|
||||
<strong>
|
||||
<%= if @contrast_warning == :poor do %>
|
||||
Text may be hard to read
|
||||
<% else %>
|
||||
Text contrast could be better
|
||||
<% end %>
|
||||
</strong>
|
||||
<p>
|
||||
Try switching to a
|
||||
<%= if @theme_settings.mood == "dark" do %>
|
||||
lighter mood
|
||||
<% else %>
|
||||
dark mood
|
||||
<% end %>
|
||||
or choosing a different image.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="admin-stack admin-stack-md theme-subfield">
|
||||
<form phx-change={@event_prefix <> "update_branding"} phx-value-field="header_zoom">
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Zoom</span>
|
||||
<span class="theme-slider-value">{@theme_settings.header_zoom}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
max="200"
|
||||
value={@theme_settings.header_zoom}
|
||||
name="header_zoom"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
<%= if @theme_settings.header_zoom > 100 do %>
|
||||
<form
|
||||
phx-change={@event_prefix <> "update_branding"}
|
||||
phx-value-field="header_position_x"
|
||||
>
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Horizontal position</span>
|
||||
<span class="theme-slider-value">{@theme_settings.header_position_x}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={@theme_settings.header_position_x}
|
||||
name="header_position_x"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
<form
|
||||
phx-change={@event_prefix <> "update_branding"}
|
||||
phx-value-field="header_position_y"
|
||||
>
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Vertical position</span>
|
||||
<span class="theme-slider-value">{@theme_settings.header_position_y}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={@theme_settings.header_position_y}
|
||||
name="header_position_y"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= for entry <- @uploads.theme_header_upload.entries do %>
|
||||
<.upload_progress entry={entry} upload_name="theme_header_upload" />
|
||||
<%= for err <- upload_errors(@uploads.theme_header_upload, entry) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= for err <- upload_errors(@uploads.theme_header_upload) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Shared upload progress component
|
||||
attr :entry, :map, required: true
|
||||
attr :upload_name, :string, required: true
|
||||
|
||||
defp upload_progress(assigns) do
|
||||
~H"""
|
||||
<div class="theme-progress">
|
||||
<div class="theme-progress-bar">
|
||||
<div class="theme-progress-fill" style={"width: #{@entry.progress}%"}></div>
|
||||
</div>
|
||||
<span class="admin-text-secondary">{@entry.progress}%</span>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="theme_cancel_upload"
|
||||
phx-value-ref={@entry.ref}
|
||||
phx-value-upload={@upload_name}
|
||||
class="theme-upload-cancel"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp error_to_string(:too_large), do: "File is too large"
|
||||
defp error_to_string(:too_many_files), do: "Too many files"
|
||||
defp error_to_string(:not_accepted), do: "File type not accepted"
|
||||
defp error_to_string(err), do: inspect(err)
|
||||
|
||||
# ── Announcement Bar Editor ─────────────────────────────────────────
|
||||
|
||||
attr :settings, :map, required: true
|
||||
|
||||
@ -18,29 +18,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
|
||||
# ── Quick Settings ─────────────────────────────────────────────────
|
||||
# These are the core settings shown in both compact and full modes.
|
||||
|
||||
@doc """
|
||||
Renders the shop name input field.
|
||||
"""
|
||||
attr :site_name, :string, required: true
|
||||
attr :event_prefix, :string, default: ""
|
||||
|
||||
def shop_name_input(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section">
|
||||
<label class="theme-section-label">Shop name</label>
|
||||
<form phx-change={@event_prefix <> "update_setting"} phx-value-field="site_name">
|
||||
<input
|
||||
type="text"
|
||||
name="site_name"
|
||||
value={@site_name}
|
||||
placeholder="Your shop name"
|
||||
class="admin-input"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the preset grid for quick theme switching.
|
||||
"""
|
||||
@ -211,19 +188,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
|
||||
~H"""
|
||||
<div class="editor-theme-content">
|
||||
<%= if @theme_settings do %>
|
||||
<.shop_name_input site_name={@site_name} event_prefix={@event_prefix} />
|
||||
|
||||
<.branding_section
|
||||
theme_settings={@theme_settings}
|
||||
uploads={@uploads}
|
||||
logo_image={@logo_image}
|
||||
header_image={@header_image}
|
||||
icon_image={@icon_image}
|
||||
contrast_warning={@contrast_warning}
|
||||
site_name={@site_name}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
|
||||
<.preset_grid presets={@presets} active_preset={@active_preset} event_prefix={@event_prefix} />
|
||||
|
||||
<.color_picker
|
||||
@ -257,334 +221,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
|
||||
"""
|
||||
end
|
||||
|
||||
# ── Branding Section (Logo, Header, Icon) ───────────────────────────
|
||||
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :uploads, :map, default: nil
|
||||
attr :logo_image, :map, default: nil
|
||||
attr :header_image, :map, default: nil
|
||||
attr :icon_image, :map, default: nil
|
||||
attr :contrast_warning, :atom, default: :ok
|
||||
attr :site_name, :string, default: ""
|
||||
attr :event_prefix, :string, default: "theme_"
|
||||
|
||||
defp branding_section(assigns) do
|
||||
~H"""
|
||||
<div class="theme-section theme-section-compact">
|
||||
<label class="theme-section-label">Logo & branding</label>
|
||||
|
||||
<div class="admin-stack admin-stack-sm theme-field">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.show_site_name}
|
||||
phx-click={@event_prefix <> "toggle_setting"}
|
||||
phx-value-field="show_site_name"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Show shop name</span>
|
||||
</label>
|
||||
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.show_logo}
|
||||
phx-click={@event_prefix <> "toggle_setting"}
|
||||
phx-value-field="show_logo"
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Show logo</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<%= if @theme_settings.show_logo && @uploads do %>
|
||||
<.logo_upload_section
|
||||
uploads={@uploads}
|
||||
logo_image={@logo_image}
|
||||
theme_settings={@theme_settings}
|
||||
site_name={@site_name}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<label class="admin-check-label theme-field">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.header_background_enabled}
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="header_background_enabled"
|
||||
phx-value-setting_value={
|
||||
if @theme_settings.header_background_enabled, do: "false", else: "true"
|
||||
}
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Header background image</span>
|
||||
</label>
|
||||
|
||||
<%= if @theme_settings.header_background_enabled && @uploads do %>
|
||||
<.header_upload_section
|
||||
uploads={@uploads}
|
||||
header_image={@header_image}
|
||||
theme_settings={@theme_settings}
|
||||
contrast_warning={@contrast_warning}
|
||||
event_prefix={@event_prefix}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Logo upload sub-section
|
||||
attr :uploads, :map, required: true
|
||||
attr :logo_image, :map, default: nil
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :site_name, :string, default: ""
|
||||
attr :event_prefix, :string, default: "theme_"
|
||||
|
||||
defp logo_upload_section(assigns) do
|
||||
~H"""
|
||||
<div class="theme-subsection">
|
||||
<span class="theme-slider-label theme-block-label">Upload logo (SVG or PNG)</span>
|
||||
<div class="admin-row admin-row-lg">
|
||||
<form phx-change="noop" phx-submit="noop" class="admin-fill">
|
||||
<label class="theme-upload-label theme-upload-label-compact">
|
||||
<span>Choose file...</span>
|
||||
<.live_file_input upload={@uploads.theme_logo_upload} class="hidden" />
|
||||
</label>
|
||||
</form>
|
||||
<%= if @logo_image do %>
|
||||
<div class="theme-thumb theme-thumb-logo theme-thumb-compact">
|
||||
<img src={"/image_cache/#{@logo_image.id}.webp"} alt={@site_name} />
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "remove_logo"}
|
||||
class="theme-remove-btn"
|
||||
title="Remove logo"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= for entry <- @uploads.theme_logo_upload.entries do %>
|
||||
<.upload_progress entry={entry} upload_name="theme_logo_upload" />
|
||||
<%= for err <- upload_errors(@uploads.theme_logo_upload, entry) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= for err <- upload_errors(@uploads.theme_logo_upload) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
|
||||
<%= if @logo_image do %>
|
||||
<form
|
||||
phx-change={@event_prefix <> "update_setting"}
|
||||
phx-value-field="logo_size"
|
||||
class="theme-subfield"
|
||||
>
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Logo size</span>
|
||||
<span class="theme-slider-value">{@theme_settings.logo_size}px</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="24"
|
||||
max="120"
|
||||
value={@theme_settings.logo_size}
|
||||
name="logo_size"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<%= if @logo_image.is_svg do %>
|
||||
<div class="theme-subfield">
|
||||
<label class="admin-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={@theme_settings.logo_recolor}
|
||||
phx-click={@event_prefix <> "update_setting"}
|
||||
phx-value-field="logo_recolor"
|
||||
phx-value-setting_value={if @theme_settings.logo_recolor, do: "false", else: "true"}
|
||||
class="admin-checkbox admin-checkbox-sm"
|
||||
/>
|
||||
<span class="theme-check-text">Recolour logo</span>
|
||||
</label>
|
||||
|
||||
<%= if @theme_settings.logo_recolor do %>
|
||||
<form
|
||||
id="logo-color-form-compact"
|
||||
phx-change={@event_prefix <> "update_color"}
|
||||
phx-value-field="logo_color"
|
||||
phx-hook="ColorSync"
|
||||
class="theme-color-row theme-subfield-sm"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
name="value"
|
||||
value={@theme_settings.logo_color}
|
||||
class="theme-color-swatch theme-color-swatch-sm"
|
||||
/>
|
||||
<span class="theme-color-value">{@theme_settings.logo_color}</span>
|
||||
</form>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Header upload sub-section
|
||||
attr :uploads, :map, required: true
|
||||
attr :header_image, :map, default: nil
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :contrast_warning, :atom, default: :ok
|
||||
attr :event_prefix, :string, default: "theme_"
|
||||
|
||||
defp header_upload_section(assigns) do
|
||||
~H"""
|
||||
<div class="theme-subsection">
|
||||
<span class="theme-slider-label theme-block-label">Upload header image</span>
|
||||
<form phx-change="noop" phx-submit="noop">
|
||||
<label class="theme-upload-label theme-upload-label-compact">
|
||||
<span>Choose file...</span>
|
||||
<.live_file_input upload={@uploads.theme_header_upload} class="hidden" />
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<%= if @header_image do %>
|
||||
<div class="theme-thumb theme-thumb-cover theme-thumb-header theme-thumb-compact">
|
||||
<img src={"/image_cache/#{@header_image.id}.webp"} alt="" />
|
||||
<button
|
||||
type="button"
|
||||
phx-click={@event_prefix <> "remove_header"}
|
||||
class="theme-remove-btn"
|
||||
title="Remove header background"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%= if @contrast_warning != :ok do %>
|
||||
<div class="theme-contrast-warning theme-contrast-warning-compact">
|
||||
<strong>
|
||||
<%= if @contrast_warning == :poor do %>
|
||||
Text may be hard to read
|
||||
<% else %>
|
||||
Text contrast could be better
|
||||
<% end %>
|
||||
</strong>
|
||||
<p>
|
||||
Try switching to a
|
||||
<%= if @theme_settings.mood == "dark" do %>
|
||||
lighter mood
|
||||
<% else %>
|
||||
dark mood
|
||||
<% end %>
|
||||
or choosing a different image.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="admin-stack admin-stack-md theme-subfield">
|
||||
<form phx-change={@event_prefix <> "update_setting"} phx-value-field="header_zoom">
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Zoom</span>
|
||||
<span class="theme-slider-value">{@theme_settings.header_zoom}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
max="200"
|
||||
value={@theme_settings.header_zoom}
|
||||
name="header_zoom"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
<%= if @theme_settings.header_zoom > 100 do %>
|
||||
<form
|
||||
phx-change={@event_prefix <> "update_setting"}
|
||||
phx-value-field="header_position_x"
|
||||
>
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Horizontal position</span>
|
||||
<span class="theme-slider-value">{@theme_settings.header_position_x}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={@theme_settings.header_position_x}
|
||||
name="header_position_x"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
<form
|
||||
phx-change={@event_prefix <> "update_setting"}
|
||||
phx-value-field="header_position_y"
|
||||
>
|
||||
<div class="theme-slider-header">
|
||||
<span class="theme-slider-label">Vertical position</span>
|
||||
<span class="theme-slider-value">{@theme_settings.header_position_y}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={@theme_settings.header_position_y}
|
||||
name="header_position_y"
|
||||
class="admin-range"
|
||||
/>
|
||||
</form>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= for entry <- @uploads.theme_header_upload.entries do %>
|
||||
<.upload_progress entry={entry} upload_name="theme_header_upload" />
|
||||
<%= for err <- upload_errors(@uploads.theme_header_upload, entry) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= for err <- upload_errors(@uploads.theme_header_upload) do %>
|
||||
<p class="theme-error-text">{error_to_string(err)}</p>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Shared upload progress component
|
||||
attr :entry, :map, required: true
|
||||
attr :upload_name, :string, required: true
|
||||
|
||||
defp upload_progress(assigns) do
|
||||
~H"""
|
||||
<div class="theme-progress">
|
||||
<div class="theme-progress-bar">
|
||||
<div class="theme-progress-fill" style={"width: #{@entry.progress}%"}></div>
|
||||
</div>
|
||||
<span class="admin-text-secondary">{@entry.progress}%</span>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="theme_cancel_upload"
|
||||
phx-value-ref={@entry.ref}
|
||||
phx-value-upload={@upload_name}
|
||||
class="theme-upload-cancel"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp error_to_string(:too_large), do: "File is too large"
|
||||
defp error_to_string(:too_many_files), do: "Too many files"
|
||||
defp error_to_string(:not_accepted), do: "File type not accepted"
|
||||
defp error_to_string(err), do: inspect(err)
|
||||
|
||||
# ── Full Customise Accordion ───────────────────────────────────────
|
||||
# Advanced settings groups for admin theme page.
|
||||
|
||||
|
||||
@ -1127,6 +1127,63 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
# ── Site branding events (route to theme state) ─────────────────────
|
||||
|
||||
# Handle shop name and theme branding settings from Site tab
|
||||
defp handle_site_action("update_branding", %{"field" => "site_name"} = params, socket) do
|
||||
# Site name updates Settings immediately (same as theme tab)
|
||||
value = params["site_name"]
|
||||
|
||||
if value do
|
||||
Settings.put_setting("site_name", value, "string")
|
||||
{:halt, assign(socket, :site_name, value)}
|
||||
else
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_site_action("update_branding", %{"field" => field} = params, socket) do
|
||||
# Route branding setting changes to theme state
|
||||
value = params[field] || params["setting_value"]
|
||||
|
||||
if value do
|
||||
field_atom = String.to_existing_atom(field)
|
||||
update_theme_setting(socket, %{field_atom => value}, field)
|
||||
else
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_site_action("toggle_branding", %{"field" => field}, socket) do
|
||||
# Route toggle to theme state (same logic as theme_toggle_setting)
|
||||
field_atom = String.to_existing_atom(field)
|
||||
current_value = Map.get(socket.assigns.theme_editor_settings, field_atom)
|
||||
new_value = !current_value
|
||||
|
||||
# Prevent turning off show_site_name when there's no logo
|
||||
if field_atom == :show_site_name && new_value == false && !has_valid_logo?(socket) do
|
||||
{:halt, socket}
|
||||
else
|
||||
update_theme_setting(socket, %{field_atom => new_value}, field)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_site_action("update_color", %{"field" => field, "value" => value}, socket) do
|
||||
# Route color updates to theme state
|
||||
field_atom = String.to_existing_atom(field)
|
||||
update_theme_setting(socket, %{field_atom => value}, field)
|
||||
end
|
||||
|
||||
defp handle_site_action("remove_logo", _params, socket) do
|
||||
# Delegate to theme action handler
|
||||
handle_theme_action("remove_logo", %{}, socket)
|
||||
end
|
||||
|
||||
defp handle_site_action("remove_header", _params, socket) do
|
||||
# Delegate to theme action handler
|
||||
handle_theme_action("remove_header", %{}, socket)
|
||||
end
|
||||
|
||||
# Catch-all for unknown site actions
|
||||
defp handle_site_action(_action, _params, socket), do: {:halt, socket}
|
||||
|
||||
|
||||
@ -226,6 +226,13 @@ defmodule BerrypodWeb.PageRenderer do
|
||||
<BerrypodWeb.ShopComponents.SiteEditor.site_editor
|
||||
site_state={@site_state}
|
||||
site_nav_pages={@site_nav_pages}
|
||||
site_name={@site_name}
|
||||
theme_settings={@theme_editor_settings}
|
||||
logo_image={@theme_editor_logo_image}
|
||||
header_image={@theme_editor_header_image}
|
||||
icon_image={@theme_editor_icon_image}
|
||||
contrast_warning={@theme_editor_contrast_warning}
|
||||
uploads={@uploads}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user