add image uploads to on-site theme editor and fix scroll on navigation
All checks were successful
deploy / deploy (push) Successful in 1m27s

Phase 4 of unified editing: image upload handling in hook context.
- Configure uploads in Shop.Page mount for logo, header, icon
- Add upload UI components to theme_editor compact_editor
- Pass uploads through page_renderer to theme editor
- Add cancel_upload handler to PageEditorHook

Also fixes scroll position not resetting on patch navigation:
- Push scroll-top event when path changes in handle_params
- JS listener scrolls window to top instantly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-09 19:45:43 +00:00
parent 89c411e0fc
commit 378b3fdb6b
6 changed files with 543 additions and 8 deletions

View File

@@ -203,12 +203,29 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
attr :site_name, :string, default: ""
attr :customise_open, :boolean, default: false
attr :event_prefix, :string, default: "theme_"
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
def compact_editor(assigns) 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
@@ -235,12 +252,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do
customise_open={@customise_open}
event_prefix={@event_prefix}
/>
<div class="theme-section">
<p class="admin-text-secondary">
For logo and header image uploads, <a href="/admin/theme" class="admin-link">visit the full theme editor</a>.
</p>
</div>
<% else %>
<p class="admin-text-secondary">Loading theme settings...</p>
<% end %>
@@ -248,6 +259,334 @@ 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.