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

@@ -9,6 +9,8 @@ defmodule BerrypodWeb.Shop.Page do
use BerrypodWeb, :live_view
alias BerrypodWeb.Shop.Pages
alias Berrypod.{Media, Settings}
alias Berrypod.Workers.FaviconGeneratorWorker
# Map live_action atoms to page handler modules
@page_modules %{
@@ -35,14 +37,159 @@ defmodule BerrypodWeb.Shop.Page do
@impl true
def mount(_params, session, socket) do
# Store session for pages that need it (orders, order_detail)
{:ok, assign(socket, :_session, session)}
socket = assign(socket, :_session, session)
# Configure uploads only for admin users (theme editor image uploads)
socket =
if socket.assigns[:is_admin] do
socket
|> allow_upload(:theme_logo_upload,
accept: ~w(.png .jpg .jpeg .webp .svg),
max_entries: 1,
max_file_size: 2_000_000,
auto_upload: true,
progress: &handle_theme_upload_progress/3
)
|> allow_upload(:theme_header_upload,
accept: ~w(.png .jpg .jpeg .webp),
max_entries: 1,
max_file_size: 5_000_000,
auto_upload: true,
progress: &handle_theme_upload_progress/3
)
|> allow_upload(:theme_icon_upload,
accept: ~w(.png .jpg .jpeg .webp .svg),
max_entries: 1,
max_file_size: 5_000_000,
auto_upload: true,
progress: &handle_theme_upload_progress/3
)
else
socket
end
{:ok, socket}
end
# Handle theme image upload progress (logo, header, icon)
defp handle_theme_upload_progress(:theme_logo_upload, entry, socket) do
if entry.done? do
consume_uploaded_entries(socket, :theme_logo_upload, fn %{path: path}, entry ->
case Media.upload_from_entry(path, entry, "logo") do
{:ok, image} ->
Settings.update_theme_settings(%{logo_image_id: image.id})
{:ok, image}
{:error, _} = error ->
error
end
end)
|> case do
[image | _] ->
# Trigger favicon generation if using logo as icon
if socket.assigns[:theme_editor_settings] &&
socket.assigns.theme_editor_settings.use_logo_as_icon do
enqueue_favicon_generation(image.id)
end
{:noreply,
assign(socket, :theme_editor_logo_image, image) |> assign(:logo_image, image)}
_ ->
{:noreply, socket}
end
else
{:noreply, socket}
end
end
defp handle_theme_upload_progress(:theme_header_upload, entry, socket) do
if entry.done? do
consume_uploaded_entries(socket, :theme_header_upload, fn %{path: path}, entry ->
case Media.upload_from_entry(path, entry, "header") do
{:ok, image} ->
Settings.update_theme_settings(%{header_image_id: image.id})
{:ok, image}
{:error, _} = error ->
error
end
end)
|> case do
[image | _] ->
socket =
socket
|> assign(:theme_editor_header_image, image)
|> assign(:header_image, image)
|> recompute_header_contrast()
{:noreply, socket}
_ ->
{:noreply, socket}
end
else
{:noreply, socket}
end
end
defp handle_theme_upload_progress(:theme_icon_upload, entry, socket) do
if entry.done? do
consume_uploaded_entries(socket, :theme_icon_upload, fn %{path: path}, entry ->
case Media.upload_from_entry(path, entry, "icon") do
{:ok, image} ->
Settings.update_theme_settings(%{icon_image_id: image.id})
{:ok, image}
{:error, _} = error ->
error
end
end)
|> case do
[image | _] ->
enqueue_favicon_generation(image.id)
{:noreply,
assign(socket, :theme_editor_icon_image, image) |> assign(:icon_image, image)}
_ ->
{:noreply, socket}
end
else
{:noreply, socket}
end
end
defp enqueue_favicon_generation(source_image_id) do
%{source_image_id: source_image_id}
|> FaviconGeneratorWorker.new()
|> Oban.insert()
end
defp recompute_header_contrast(socket) do
header_image = socket.assigns[:theme_editor_header_image]
theme_settings = socket.assigns[:theme_editor_settings]
warning =
if theme_settings && theme_settings.header_background_enabled && header_image do
text_color = Berrypod.Theme.Contrast.text_color_for_mood(theme_settings.mood)
colors = Berrypod.Theme.Contrast.parse_dominant_colors(header_image.dominant_colors)
Berrypod.Theme.Contrast.analyze_header_contrast(colors, text_color)
else
:ok
end
assign(socket, :theme_editor_contrast_warning, warning)
end
@impl true
def handle_params(params, uri, socket) do
action = socket.assigns.live_action
prev_action = socket.assigns[:_current_page_action]
prev_path = socket.assigns[:_current_path]
module = @page_modules[action]
parsed_uri = URI.parse(uri)
current_path = parsed_uri.path
# Clean up previous page if needed (e.g., unsubscribe from PubSub)
socket = maybe_cleanup_previous_page(socket, prev_action)
@@ -73,6 +220,16 @@ defmodule BerrypodWeb.Shop.Page do
# After page init, sync editor state if editing and page changed
socket = maybe_sync_editing_blocks(socket)
# Scroll to top on navigation (different path, not just query param changes)
socket =
if prev_path && prev_path != current_path do
Phoenix.LiveView.push_event(socket, "scroll-top", %{})
else
socket
end
socket = assign(socket, :_current_path, current_path)
# Always call handle_params for URL changes
case module.handle_params(params, uri, socket) do
{:noreply, socket} -> {:noreply, socket}