add Site context with social links editor and site-wide settings
Some checks failed
deploy / deploy (push) Has been cancelled
Some checks failed
deploy / deploy (push) Has been cancelled
- Add Site context for managing site-wide content (social links, nav items, announcement bar, footer content) - Add SocialLink schema with URL normalization and platform auto-detection supporting 40+ platforms via host and 25+ via URI scheme - Add NavItem schema for header/footer navigation (editor UI coming next) - Add SiteEditor component with collapsible sections for each content type - Wire social links card block and footer to use database data - Filter empty URLs from display in shop components - Add DetailsPreserver hook to preserve collapsible section state - Add comprehensive tests for Site context and SocialLink functions - Remove unused helper functions from onboarding to fix compiler warnings - Move sync_edit_url_param helper to group handle_editor_event clauses Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
import Phoenix.Component, only: [assign: 3]
|
||||
import Phoenix.LiveView, only: [attach_hook: 4, push_navigate: 2, push_patch: 2]
|
||||
|
||||
alias Berrypod.{Media, Settings}
|
||||
alias Berrypod.{Media, Settings, Site}
|
||||
alias Berrypod.Pages
|
||||
alias Berrypod.Pages.{BlockEditor, BlockTypes, Defaults}
|
||||
alias Berrypod.Theme.{Contrast, CSSGenerator, Presets}
|
||||
@@ -53,6 +53,8 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
|> assign(:editor_active_tab, :page)
|
||||
# Theme editing state
|
||||
|> assign(:theme_editing, false)
|
||||
|> assign(:theme_dirty, false)
|
||||
|> assign(:theme_editor_original, nil)
|
||||
|> assign(:theme_editor_settings, nil)
|
||||
|> assign(:theme_editor_active_preset, nil)
|
||||
|> assign(:theme_editor_logo_image, nil)
|
||||
@@ -64,6 +66,21 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
# Settings editing state
|
||||
|> assign(:settings_dirty, false)
|
||||
|> assign(:settings_save_status, :idle)
|
||||
# Site editing state
|
||||
|> assign(:site_editing, false)
|
||||
|> assign(:site_dirty, false)
|
||||
|> assign(:site_editor_original, nil)
|
||||
|> assign(:site_header_nav, [])
|
||||
|> assign(:site_footer_nav, [])
|
||||
|> assign(:site_social_links, [])
|
||||
|> assign(:site_announcement_text, "")
|
||||
|> assign(:site_announcement_link, "")
|
||||
|> assign(:site_announcement_style, "info")
|
||||
|> assign(:site_footer_about, "")
|
||||
|> assign(:site_footer_copyright, "")
|
||||
|> assign(:site_footer_show_newsletter, true)
|
||||
# Navigation warning state
|
||||
|> assign(:editor_nav_blocked, nil)
|
||||
|> attach_hook(:editor_params, :handle_params, &handle_editor_params/3)
|
||||
|> attach_hook(:editor_events, :handle_event, &handle_editor_event/3)
|
||||
|> attach_hook(:editor_info, :handle_info, &handle_editor_info/2)
|
||||
@@ -119,6 +136,12 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
|> assign(:editor_active_tab, :settings)
|
||||
|> maybe_enter_theme_mode()
|
||||
|
||||
"site" ->
|
||||
socket
|
||||
|> assign(:editor_sheet_state, :open)
|
||||
|> assign(:editor_active_tab, :site)
|
||||
|> maybe_enter_site_mode()
|
||||
|
||||
nil ->
|
||||
# No edit param - collapse the editor (supports browser back)
|
||||
assign(socket, :editor_sheet_state, :collapsed)
|
||||
@@ -153,6 +176,14 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_enter_site_mode(socket) do
|
||||
if socket.assigns.site_editing do
|
||||
socket
|
||||
else
|
||||
load_site_state(socket)
|
||||
end
|
||||
end
|
||||
|
||||
# ── handle_info ─────────────────────────────────────────────────
|
||||
|
||||
defp handle_editor_info(:editor_clear_save_status, socket) do
|
||||
@@ -192,18 +223,6 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
end
|
||||
end
|
||||
|
||||
# Sync URL ?edit param with editor state
|
||||
defp sync_edit_url_param(socket, :collapsed) do
|
||||
path = socket.assigns.editor_current_path || "/"
|
||||
push_patch(socket, to: path)
|
||||
end
|
||||
|
||||
defp sync_edit_url_param(socket, :open) do
|
||||
path = socket.assigns.editor_current_path || "/"
|
||||
tab = socket.assigns.editor_active_tab
|
||||
push_patch(socket, to: "#{path}?edit=#{tab}")
|
||||
end
|
||||
|
||||
# Tab switching for unified editor
|
||||
defp handle_editor_event("editor_set_tab", %{"tab" => tab_str}, socket) do
|
||||
if socket.assigns.is_admin do
|
||||
@@ -249,6 +268,26 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
end
|
||||
|
||||
assign(socket, :editor_active_tab, :settings)
|
||||
|
||||
:site ->
|
||||
# Site tab shows site-wide content editors
|
||||
# Load theme state for branding settings (will be moved here from theme tab)
|
||||
socket =
|
||||
if socket.assigns.theme_editing do
|
||||
socket
|
||||
else
|
||||
load_theme_state(socket)
|
||||
end
|
||||
|
||||
# Load site state if not already loaded
|
||||
socket =
|
||||
if socket.assigns.site_editing do
|
||||
socket
|
||||
else
|
||||
load_site_state(socket)
|
||||
end
|
||||
|
||||
assign(socket, :editor_active_tab, :site)
|
||||
end
|
||||
|
||||
# Open the sheet and sync URL with new tab
|
||||
@@ -276,6 +315,54 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
end
|
||||
end
|
||||
|
||||
# Unified save works for all tabs regardless of editing mode
|
||||
defp handle_editor_event("editor_save_all", _params, socket) do
|
||||
if socket.assigns.is_admin do
|
||||
socket = save_all_tabs(socket)
|
||||
{:halt, socket}
|
||||
else
|
||||
{:cont, socket}
|
||||
end
|
||||
end
|
||||
|
||||
# Navigation blocked by unsaved changes
|
||||
defp handle_editor_event("editor_nav_blocked", %{"href" => href}, socket) do
|
||||
{:halt, assign(socket, :editor_nav_blocked, href)}
|
||||
end
|
||||
|
||||
defp handle_editor_event("editor_save_and_navigate", _params, socket) do
|
||||
href = socket.assigns.editor_nav_blocked
|
||||
socket = save_all_tabs(socket)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:editor_nav_blocked, nil)
|
||||
|> Phoenix.LiveView.push_event("editor_navigate", %{href: href})
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_editor_event("editor_discard_and_navigate", _params, socket) do
|
||||
href = socket.assigns.editor_nav_blocked
|
||||
socket = revert_all_tabs(socket)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:editor_nav_blocked, nil)
|
||||
|> Phoenix.LiveView.push_event("editor_navigate", %{href: href})
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_editor_event("editor_cancel_navigate", _params, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:editor_nav_blocked, nil)
|
||||
|> assign(:editor_sheet_state, :open)
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_editor_event("editor_" <> action, params, socket) do
|
||||
if socket.assigns.editing do
|
||||
handle_editor_action(action, params, socket)
|
||||
@@ -302,8 +389,29 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
end
|
||||
end
|
||||
|
||||
# Site editing events (announcement bar, footer, etc.)
|
||||
defp handle_editor_event("site_" <> action, params, socket) do
|
||||
if socket.assigns.is_admin do
|
||||
handle_site_action(action, params, socket)
|
||||
else
|
||||
{:cont, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_editor_event(_event, _params, socket), do: {:cont, socket}
|
||||
|
||||
# Sync URL ?edit param with editor state
|
||||
defp sync_edit_url_param(socket, :collapsed) do
|
||||
path = socket.assigns.editor_current_path || "/"
|
||||
push_patch(socket, to: path)
|
||||
end
|
||||
|
||||
defp sync_edit_url_param(socket, :open) do
|
||||
path = socket.assigns.editor_current_path || "/"
|
||||
tab = socket.assigns.editor_active_tab
|
||||
push_patch(socket, to: "#{path}?edit=#{tab}")
|
||||
end
|
||||
|
||||
# ── Block manipulation actions ───────────────────────────────────
|
||||
|
||||
defp handle_editor_action("move_up", %{"id" => id}, socket) do
|
||||
@@ -603,6 +711,11 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_editor_action("save_all", _params, socket) do
|
||||
socket = save_all_tabs(socket)
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_editor_action("reset_defaults", _params, socket) do
|
||||
slug = socket.assigns.page.slug
|
||||
default_blocks = Berrypod.Pages.Defaults.for_slug(slug).blocks
|
||||
@@ -627,8 +740,16 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
defp handle_theme_action("apply_preset", %{"preset" => preset_name}, socket) do
|
||||
preset_atom = String.to_existing_atom(preset_name)
|
||||
|
||||
case Settings.apply_preset(preset_atom) do
|
||||
{:ok, theme_settings} ->
|
||||
# Get preset values and apply in-memory (don't persist yet)
|
||||
case Presets.get(preset_atom) do
|
||||
nil ->
|
||||
{:halt, socket}
|
||||
|
||||
preset_values ->
|
||||
# Merge preset values into current settings
|
||||
current = socket.assigns.theme_editor_settings
|
||||
theme_settings = struct(current, preset_values)
|
||||
|
||||
generated_css =
|
||||
CSSGenerator.generate(theme_settings, &BerrypodWeb.Endpoint.static_path/1)
|
||||
|
||||
@@ -641,14 +762,12 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
|> assign(:theme_editor_settings, theme_settings)
|
||||
|> assign(:theme_editor_active_preset, preset_atom)
|
||||
|> assign(:theme_editor_contrast_warning, contrast_warning)
|
||||
|> assign(:theme_dirty, true)
|
||||
# Update shop state so layout reflects changes live
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:generated_css, generated_css)
|
||||
|
||||
{:halt, socket}
|
||||
|
||||
{:error, _} ->
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -658,6 +777,8 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
socket
|
||||
)
|
||||
when field in @standalone_settings do
|
||||
# Standalone settings (site_name, site_description) save immediately for now
|
||||
# TODO: Track these separately for proper revert
|
||||
Settings.put_setting(field, value, "string")
|
||||
# Also update the main assigns so ThemeHook sees the change
|
||||
{:halt, assign(socket, String.to_existing_atom(field), value)}
|
||||
@@ -677,6 +798,8 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
value = params[field]
|
||||
|
||||
if value do
|
||||
# Standalone settings save immediately for now
|
||||
# TODO: Track these separately for proper revert
|
||||
Settings.put_setting(field, value, "string")
|
||||
{:halt, assign(socket, String.to_existing_atom(field), value)}
|
||||
else
|
||||
@@ -719,13 +842,14 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
end
|
||||
|
||||
defp handle_theme_action("remove_logo", _params, socket) do
|
||||
# Delete the image immediately (this is a destructive action)
|
||||
if logo = socket.assigns.theme_editor_logo_image do
|
||||
Media.delete_image(logo)
|
||||
end
|
||||
|
||||
{:ok, theme_settings} =
|
||||
Settings.update_theme_settings(%{logo_image_id: nil, show_site_name: true})
|
||||
|
||||
# Update settings in memory only
|
||||
current = socket.assigns.theme_editor_settings
|
||||
theme_settings = %{current | logo_image_id: nil, show_site_name: true}
|
||||
generated_css = CSSGenerator.generate(theme_settings)
|
||||
|
||||
socket =
|
||||
@@ -733,38 +857,51 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
|> assign(:theme_editor_logo_image, nil)
|
||||
|> assign(:logo_image, nil)
|
||||
|> assign(:theme_editor_settings, theme_settings)
|
||||
|> assign(:theme_dirty, true)
|
||||
|> assign(:generated_css, generated_css)
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_theme_action("remove_header", _params, socket) do
|
||||
# Delete the image immediately (this is a destructive action)
|
||||
if header = socket.assigns.theme_editor_header_image do
|
||||
Media.delete_image(header)
|
||||
end
|
||||
|
||||
Settings.update_theme_settings(%{header_image_id: nil})
|
||||
# Update settings in memory only
|
||||
current = socket.assigns.theme_editor_settings
|
||||
theme_settings = %{current | header_image_id: nil}
|
||||
generated_css = CSSGenerator.generate(theme_settings)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:theme_editor_header_image, nil)
|
||||
|> assign(:header_image, nil)
|
||||
|> assign(:theme_editor_settings, theme_settings)
|
||||
|> assign(:theme_dirty, true)
|
||||
|> assign(:theme_editor_contrast_warning, :ok)
|
||||
|> assign(:generated_css, generated_css)
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_theme_action("remove_icon", _params, socket) do
|
||||
# Delete the image immediately (this is a destructive action)
|
||||
if icon = socket.assigns.theme_editor_icon_image do
|
||||
Media.delete_image(icon)
|
||||
end
|
||||
|
||||
Settings.update_theme_settings(%{icon_image_id: nil})
|
||||
# Update settings in memory only
|
||||
current = socket.assigns.theme_editor_settings
|
||||
theme_settings = %{current | icon_image_id: nil}
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:theme_editor_icon_image, nil)
|
||||
|> assign(:icon_image, nil)
|
||||
|> assign(:theme_editor_settings, theme_settings)
|
||||
|> assign(:theme_dirty, true)
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
@@ -853,6 +990,222 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
# Catch-all for unknown settings actions
|
||||
defp handle_settings_action(_action, _params, socket), do: {:halt, socket}
|
||||
|
||||
# --- Site tab event handlers ---
|
||||
|
||||
defp handle_site_action("update", %{"site" => site_params}, socket) do
|
||||
socket = handle_site_update(socket, site_params)
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp handle_site_action("update", _params, socket), do: {:halt, socket}
|
||||
|
||||
# Social link CRUD operations (persist immediately like images)
|
||||
defp handle_site_action("add_social_link", _params, socket) do
|
||||
# Create with "custom" platform and blank URL
|
||||
# User will paste their link, which auto-detects the platform
|
||||
position = length(socket.assigns.site_social_links)
|
||||
|
||||
attrs = %{
|
||||
platform: "custom",
|
||||
url: "",
|
||||
position: position
|
||||
}
|
||||
|
||||
case Site.create_social_link(attrs) do
|
||||
{:ok, link} ->
|
||||
links = socket.assigns.site_social_links ++ [link]
|
||||
{:halt, assign(socket, :site_social_links, links)}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_site_action("update_social_link", %{"id" => id} = params, socket) do
|
||||
link = Enum.find(socket.assigns.site_social_links, &(&1.id == id))
|
||||
|
||||
if link do
|
||||
# Extract the nested params from the form field name
|
||||
link_params = params["social_link"][id] || %{}
|
||||
target = params["_target"] || []
|
||||
|
||||
# Check what field was changed
|
||||
url_changed? = List.last(target) == "url"
|
||||
platform_changed? = List.last(target) == "platform"
|
||||
|
||||
# Build attrs based on what changed
|
||||
attrs =
|
||||
cond do
|
||||
# URL changed - normalize and update URL, maybe auto-detect platform
|
||||
url_changed? ->
|
||||
url = link_params["url"] |> Berrypod.Site.SocialLink.normalize_url()
|
||||
detected = Berrypod.Site.SocialLink.detect_platform(url)
|
||||
|
||||
base = %{url: url}
|
||||
|
||||
# Auto-detect platform when:
|
||||
# 1. Current platform is "custom" (initial state), OR
|
||||
# 2. New URL detects to a different platform than currently set
|
||||
# (e.g., changing from github.com to twitter.com)
|
||||
should_update_platform? =
|
||||
detected &&
|
||||
detected != "custom" &&
|
||||
(link.platform == "custom" || detected != link.platform)
|
||||
|
||||
if should_update_platform? do
|
||||
Map.put(base, :platform, detected)
|
||||
else
|
||||
base
|
||||
end
|
||||
|
||||
# Platform explicitly changed by user - use their selection
|
||||
platform_changed? ->
|
||||
%{platform: link_params["platform"]}
|
||||
|
||||
# Fallback
|
||||
true ->
|
||||
%{}
|
||||
|> maybe_put(:url, link_params["url"])
|
||||
|> maybe_put(:platform, link_params["platform"])
|
||||
end
|
||||
|
||||
if attrs != %{} do
|
||||
case Site.update_social_link(link, attrs) do
|
||||
{:ok, updated_link} ->
|
||||
links =
|
||||
Enum.map(socket.assigns.site_social_links, fn l ->
|
||||
if l.id == id, do: updated_link, else: l
|
||||
end)
|
||||
|
||||
{:halt, assign(socket, :site_social_links, links)}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:halt, socket}
|
||||
end
|
||||
else
|
||||
{:halt, socket}
|
||||
end
|
||||
else
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_site_action("remove_social_link", %{"id" => id}, socket) do
|
||||
link = Enum.find(socket.assigns.site_social_links, &(&1.id == id))
|
||||
|
||||
if link do
|
||||
case Site.delete_social_link(link) do
|
||||
{:ok, _} ->
|
||||
links = Enum.reject(socket.assigns.site_social_links, &(&1.id == id))
|
||||
{:halt, assign(socket, :site_social_links, links)}
|
||||
|
||||
{:error, _} ->
|
||||
{:halt, socket}
|
||||
end
|
||||
else
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_site_action("move_social_link", %{"id" => id, "dir" => dir}, socket) do
|
||||
links = socket.assigns.site_social_links
|
||||
index = Enum.find_index(links, &(&1.id == id))
|
||||
|
||||
new_index =
|
||||
case dir do
|
||||
"up" -> max(0, index - 1)
|
||||
"down" -> min(length(links) - 1, index + 1)
|
||||
_ -> index
|
||||
end
|
||||
|
||||
if index != new_index do
|
||||
# Swap the items
|
||||
item = Enum.at(links, index)
|
||||
other = Enum.at(links, new_index)
|
||||
|
||||
reordered =
|
||||
links
|
||||
|> List.replace_at(index, other)
|
||||
|> List.replace_at(new_index, item)
|
||||
|
||||
# Persist the new order
|
||||
ids = Enum.map(reordered, & &1.id)
|
||||
Site.reorder_social_links(ids)
|
||||
|
||||
{:halt, assign(socket, :site_social_links, reordered)}
|
||||
else
|
||||
{:halt, socket}
|
||||
end
|
||||
end
|
||||
|
||||
# Catch-all for unknown site actions
|
||||
defp handle_site_action(_action, _params, socket), do: {:halt, socket}
|
||||
|
||||
defp handle_site_update(socket, params) do
|
||||
# Handle announcement bar fields (preview only, no persistence)
|
||||
socket =
|
||||
if Map.has_key?(params, "announcement_text") or
|
||||
Map.has_key?(params, "announcement_link") or
|
||||
Map.has_key?(params, "announcement_style") do
|
||||
text = params["announcement_text"] || socket.assigns.site_announcement_text
|
||||
link = params["announcement_link"] || socket.assigns.site_announcement_link
|
||||
style = params["announcement_style"] || socket.assigns.site_announcement_style
|
||||
|
||||
socket
|
||||
|> assign(:site_announcement_text, text)
|
||||
|> assign(:site_announcement_link, link)
|
||||
|> assign(:site_announcement_style, style)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
# Handle footer fields (preview only, no persistence)
|
||||
socket =
|
||||
if Map.has_key?(params, "footer_about") or
|
||||
Map.has_key?(params, "footer_copyright") or
|
||||
Map.has_key?(params, "show_newsletter") do
|
||||
about = params["footer_about"] || socket.assigns.site_footer_about
|
||||
copyright = params["footer_copyright"] || socket.assigns.site_footer_copyright
|
||||
|
||||
# Checkbox sends value when checked, absent when unchecked
|
||||
show_newsletter =
|
||||
if Map.has_key?(params, "show_newsletter") do
|
||||
params["show_newsletter"] == "true"
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:site_footer_about, about)
|
||||
|> assign(:site_footer_copyright, copyright)
|
||||
|> assign(:site_footer_show_newsletter, show_newsletter)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
# Mark as dirty if values differ from original
|
||||
socket
|
||||
|> compute_site_dirty()
|
||||
end
|
||||
|
||||
defp compute_site_dirty(socket) do
|
||||
original = socket.assigns[:site_editor_original]
|
||||
|
||||
dirty =
|
||||
if original do
|
||||
socket.assigns.site_announcement_text != original.announcement_text or
|
||||
socket.assigns.site_announcement_link != original.announcement_link or
|
||||
socket.assigns.site_announcement_style != original.announcement_style or
|
||||
socket.assigns.site_footer_about != original.footer_about or
|
||||
socket.assigns.site_footer_copyright != original.footer_copyright or
|
||||
socket.assigns.site_footer_show_newsletter != original.show_newsletter
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
assign(socket, :site_dirty, dirty)
|
||||
end
|
||||
|
||||
# Check if settings have changed from current page values
|
||||
defp has_settings_changed?(page, params) do
|
||||
page.title != (params["title"] || "") or
|
||||
@@ -881,30 +1234,34 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
assign(socket, :settings_form, form)
|
||||
end
|
||||
|
||||
# Helper to update a theme setting and regenerate CSS
|
||||
# Helper to update a theme setting in-memory (preview only, no persistence)
|
||||
defp update_theme_setting(socket, attrs, field) do
|
||||
case Settings.update_theme_settings(attrs) do
|
||||
{:ok, theme_settings} ->
|
||||
generated_css =
|
||||
CSSGenerator.generate(theme_settings, &BerrypodWeb.Endpoint.static_path/1)
|
||||
current = socket.assigns.theme_editor_settings
|
||||
|
||||
active_preset = Presets.detect_preset(theme_settings)
|
||||
# Merge attrs into current settings (convert string keys to atoms)
|
||||
theme_settings =
|
||||
Enum.reduce(attrs, current, fn {key, value}, acc ->
|
||||
atom_key = if is_binary(key), do: String.to_existing_atom(key), else: key
|
||||
Map.put(acc, atom_key, value)
|
||||
end)
|
||||
|
||||
socket =
|
||||
socket
|
||||
# Update editor state
|
||||
|> assign(:theme_editor_settings, theme_settings)
|
||||
|> assign(:theme_editor_active_preset, active_preset)
|
||||
|> maybe_recompute_contrast(field)
|
||||
# Update shop state so layout reflects changes live
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:generated_css, generated_css)
|
||||
generated_css =
|
||||
CSSGenerator.generate(theme_settings, &BerrypodWeb.Endpoint.static_path/1)
|
||||
|
||||
{:halt, socket}
|
||||
active_preset = Presets.detect_preset(theme_settings)
|
||||
|
||||
{:error, _} ->
|
||||
{:halt, socket}
|
||||
end
|
||||
socket =
|
||||
socket
|
||||
# Update editor state
|
||||
|> assign(:theme_editor_settings, theme_settings)
|
||||
|> assign(:theme_editor_active_preset, active_preset)
|
||||
|> assign(:theme_dirty, true)
|
||||
|> maybe_recompute_contrast(field)
|
||||
# Update shop state so layout reflects changes live
|
||||
|> assign(:theme_settings, theme_settings)
|
||||
|> assign(:generated_css, generated_css)
|
||||
|
||||
{:halt, socket}
|
||||
end
|
||||
|
||||
defp maybe_recompute_contrast(socket, field)
|
||||
@@ -1017,6 +1374,8 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
|
||||
socket
|
||||
|> assign(:theme_editing, true)
|
||||
|> assign(:theme_dirty, false)
|
||||
|> assign(:theme_editor_original, theme_settings)
|
||||
|> assign(:theme_editor_settings, theme_settings)
|
||||
|> assign(:theme_editor_active_preset, active_preset)
|
||||
|> assign(:theme_editor_logo_image, logo_image)
|
||||
@@ -1054,4 +1413,189 @@ defmodule BerrypodWeb.PageEditorHook do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# ── Site editing helpers ───────────────────────────────────────────
|
||||
|
||||
defp load_site_state(socket) do
|
||||
site_settings = Site.get_settings()
|
||||
|
||||
# Store original values for revert capability
|
||||
original = %{
|
||||
announcement_text: site_settings.announcement_text,
|
||||
announcement_link: site_settings.announcement_link,
|
||||
announcement_style: site_settings.announcement_style,
|
||||
footer_about: site_settings.footer_about,
|
||||
footer_copyright: site_settings.footer_copyright,
|
||||
show_newsletter: site_settings.show_newsletter
|
||||
}
|
||||
|
||||
socket
|
||||
|> assign(:site_editing, true)
|
||||
|> assign(:site_dirty, false)
|
||||
|> assign(:site_editor_original, original)
|
||||
|> assign(:site_header_nav, Site.list_nav_items(:header))
|
||||
|> assign(:site_footer_nav, Site.list_nav_items(:footer))
|
||||
|> assign(:site_social_links, Site.list_social_links())
|
||||
|> assign(:site_announcement_text, site_settings.announcement_text)
|
||||
|> assign(:site_announcement_link, site_settings.announcement_link)
|
||||
|> assign(:site_announcement_style, site_settings.announcement_style)
|
||||
|> assign(:site_footer_about, site_settings.footer_about)
|
||||
|> assign(:site_footer_copyright, site_settings.footer_copyright)
|
||||
|> assign(:site_footer_show_newsletter, site_settings.show_newsletter)
|
||||
|> assign(:editor_sheet_state, :open)
|
||||
end
|
||||
|
||||
# ── Unified save helpers ───────────────────────────────────────────
|
||||
|
||||
defp save_all_tabs(socket) do
|
||||
socket
|
||||
|> maybe_save_page()
|
||||
|> maybe_save_theme()
|
||||
|> maybe_save_site()
|
||||
|> assign(:editor_save_status, :saved)
|
||||
|> schedule_save_status_clear()
|
||||
end
|
||||
|
||||
defp maybe_save_page(socket) do
|
||||
if socket.assigns[:editor_dirty] do
|
||||
%{page: page, editing_blocks: blocks} = socket.assigns
|
||||
|
||||
case Pages.save_page(page.slug, %{title: page.title, blocks: blocks}) do
|
||||
{:ok, _saved_page} ->
|
||||
updated_page = Pages.get_page(page.slug)
|
||||
at_defaults = Defaults.matches_defaults?(page.slug, updated_page.blocks)
|
||||
|
||||
socket
|
||||
|> assign(:page, updated_page)
|
||||
|> assign(:editing_blocks, updated_page.blocks)
|
||||
|> assign(:editor_dirty, false)
|
||||
|> assign(:editor_at_defaults, at_defaults)
|
||||
|> assign(:editor_history, [])
|
||||
|> assign(:editor_future, [])
|
||||
|
||||
{:error, _changeset} ->
|
||||
socket
|
||||
end
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_save_theme(socket) do
|
||||
if socket.assigns[:theme_dirty] do
|
||||
case Settings.update_theme_settings(Map.from_struct(socket.assigns.theme_editor_settings)) do
|
||||
{:ok, theme_settings} ->
|
||||
socket
|
||||
|> assign(:theme_editor_original, theme_settings)
|
||||
|> assign(:theme_dirty, false)
|
||||
|
||||
{:error, _} ->
|
||||
socket
|
||||
end
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_save_site(socket) do
|
||||
if socket.assigns[:site_dirty] do
|
||||
Site.put_announcement(
|
||||
socket.assigns.site_announcement_text,
|
||||
socket.assigns.site_announcement_link,
|
||||
socket.assigns.site_announcement_style
|
||||
)
|
||||
|
||||
Site.put_footer_content(
|
||||
socket.assigns.site_footer_about,
|
||||
socket.assigns.site_footer_copyright,
|
||||
socket.assigns.site_footer_show_newsletter
|
||||
)
|
||||
|
||||
# Update original to match saved values
|
||||
original = %{
|
||||
announcement_text: socket.assigns.site_announcement_text,
|
||||
announcement_link: socket.assigns.site_announcement_link,
|
||||
announcement_style: socket.assigns.site_announcement_style,
|
||||
footer_about: socket.assigns.site_footer_about,
|
||||
footer_copyright: socket.assigns.site_footer_copyright,
|
||||
show_newsletter: socket.assigns.site_footer_show_newsletter
|
||||
}
|
||||
|
||||
socket
|
||||
|> assign(:site_editor_original, original)
|
||||
|> assign(:site_dirty, false)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp schedule_save_status_clear(socket) do
|
||||
Process.send_after(self(), :editor_clear_save_status, 2500)
|
||||
socket
|
||||
end
|
||||
|
||||
# ── Revert helpers ─────────────────────────────────────────────────
|
||||
|
||||
defp revert_all_tabs(socket) do
|
||||
socket
|
||||
|> maybe_revert_page()
|
||||
|> maybe_revert_theme()
|
||||
|> maybe_revert_site()
|
||||
end
|
||||
|
||||
defp maybe_revert_page(socket) do
|
||||
if socket.assigns[:editor_dirty] do
|
||||
# Reload the page from database
|
||||
page = Pages.get_page(socket.assigns.page.slug)
|
||||
at_defaults = Defaults.matches_defaults?(page.slug, page.blocks)
|
||||
|
||||
socket
|
||||
|> assign(:page, page)
|
||||
|> assign(:editing_blocks, page.blocks)
|
||||
|> assign(:editor_dirty, false)
|
||||
|> assign(:editor_at_defaults, at_defaults)
|
||||
|> assign(:editor_history, [])
|
||||
|> assign(:editor_future, [])
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_revert_theme(socket) do
|
||||
if socket.assigns[:theme_dirty] do
|
||||
original = socket.assigns.theme_editor_original
|
||||
generated_css = CSSGenerator.generate(original, &BerrypodWeb.Endpoint.static_path/1)
|
||||
|
||||
socket
|
||||
|> assign(:theme_editor_settings, original)
|
||||
|> assign(:theme_settings, original)
|
||||
|> assign(:generated_css, generated_css)
|
||||
|> assign(:theme_dirty, false)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_revert_site(socket) do
|
||||
if socket.assigns[:site_dirty] do
|
||||
original = socket.assigns.site_editor_original
|
||||
|
||||
socket
|
||||
|> assign(:site_announcement_text, original.announcement_text)
|
||||
|> assign(:site_announcement_link, original.announcement_link)
|
||||
|> assign(:site_announcement_style, original.announcement_style)
|
||||
|> assign(:site_footer_about, original.footer_about)
|
||||
|> assign(:site_footer_copyright, original.footer_copyright)
|
||||
|> assign(:site_footer_show_newsletter, original.show_newsletter)
|
||||
|> assign(:site_dirty, false)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
# ── Small helpers ───────────────────────────────────────────────────
|
||||
|
||||
defp maybe_put(map, _key, nil), do: map
|
||||
defp maybe_put(map, _key, ""), do: map
|
||||
defp maybe_put(map, key, value), do: Map.put(map, key, value)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user