diff --git a/PROGRESS.md b/PROGRESS.md index e6c4f9b..6aff579 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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 diff --git a/docs/plans/editor-reorganisation.md b/docs/plans/editor-reorganisation.md index 72df967..59c582b 100644 --- a/docs/plans/editor-reorganisation.md +++ b/docs/plans/editor-reorganisation.md @@ -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 diff --git a/lib/berrypod_web/components/shop_components/site_editor.ex b/lib/berrypod_web/components/shop_components/site_editor.ex index 81f0165..7da553c 100644 --- a/lib/berrypod_web/components/shop_components/site_editor.ex +++ b/lib/berrypod_web/components/shop_components/site_editor.ex @@ -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"""
<.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 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""" -
-

- Branding settings (shop name, logo, favicon) will be moved here from the Theme tab. -

-

Coming soon in the next phase.

+

Loading branding settings...

+ """ + end + + defp branding_editor(assigns) do + ~H""" +
+ <%!-- Shop name --%> +
+ +
"update_branding"} phx-value-field="site_name"> + +
+
+ + <%!-- Display toggles --%> +
+ + + +
+ + <%!-- 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 --%> + + + <%!-- 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 %>
""" 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""" +
+ Upload logo (SVG or PNG) +
+
+ +
+ <%= if @logo_image do %> + + <% end %> +
+ + <%= 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 %> +

{error_to_string(err)}

+ <% end %> + <% end %> + + <%= for err <- upload_errors(@uploads.theme_logo_upload) do %> +

{error_to_string(err)}

+ <% end %> + + <%= if @logo_image do %> +
"update_branding"} + phx-value-field="logo_size" + class="theme-subfield" + > +
+ Logo size + {@theme_settings.logo_size}px +
+ +
+ + <%= if @logo_image.is_svg do %> +
+ + + <%= if @theme_settings.logo_recolor do %> +
"update_color"} + phx-value-field="logo_color" + phx-hook="ColorSync" + class="theme-color-row theme-subfield-sm" + > + + {@theme_settings.logo_color} +
+ <% end %> +
+ <% end %> + <% end %> +
+ """ + 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""" +
+ Upload header image +
+ +
+ + <%= if @header_image do %> +
+ + +
+ + <%= if @contrast_warning != :ok do %> +
+ + <%= if @contrast_warning == :poor do %> + Text may be hard to read + <% else %> + Text contrast could be better + <% end %> + +

+ Try switching to a + <%= if @theme_settings.mood == "dark" do %> + lighter mood + <% else %> + dark mood + <% end %> + or choosing a different image. +

+
+ <% end %> + +
+
"update_branding"} phx-value-field="header_zoom"> +
+ Zoom + {@theme_settings.header_zoom}% +
+ +
+ <%= if @theme_settings.header_zoom > 100 do %> +
"update_branding"} + phx-value-field="header_position_x" + > +
+ Horizontal position + {@theme_settings.header_position_x}% +
+ +
+
"update_branding"} + phx-value-field="header_position_y" + > +
+ Vertical position + {@theme_settings.header_position_y}% +
+ +
+ <% end %> +
+ <% 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 %> +

{error_to_string(err)}

+ <% end %> + <% end %> + + <%= for err <- upload_errors(@uploads.theme_header_upload) do %> +

{error_to_string(err)}

+ <% end %> +
+ """ + end + + # Shared upload progress component + attr :entry, :map, required: true + attr :upload_name, :string, required: true + + defp upload_progress(assigns) do + ~H""" +
+
+
+
+ {@entry.progress}% + +
+ """ + 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 diff --git a/lib/berrypod_web/components/shop_components/theme_editor.ex b/lib/berrypod_web/components/shop_components/theme_editor.ex index b153bb9..d9aadcb 100644 --- a/lib/berrypod_web/components/shop_components/theme_editor.ex +++ b/lib/berrypod_web/components/shop_components/theme_editor.ex @@ -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""" -
- -
"update_setting"} phx-value-field="site_name"> - -
-
- """ - end - @doc """ Renders the preset grid for quick theme switching. """ @@ -211,19 +188,6 @@ defmodule BerrypodWeb.ShopComponents.ThemeEditor do ~H"""
<%= 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""" -
- - -
- - - -
- - <%= 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 %> - - - - <%= 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 %> -
- """ - 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""" -
- Upload logo (SVG or PNG) -
-
- -
- <%= if @logo_image do %> - - <% end %> -
- - <%= 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 %> -

{error_to_string(err)}

- <% end %> - <% end %> - - <%= for err <- upload_errors(@uploads.theme_logo_upload) do %> -

{error_to_string(err)}

- <% end %> - - <%= if @logo_image do %> -
"update_setting"} - phx-value-field="logo_size" - class="theme-subfield" - > -
- Logo size - {@theme_settings.logo_size}px -
- -
- - <%= if @logo_image.is_svg do %> -
- - - <%= if @theme_settings.logo_recolor do %> -
"update_color"} - phx-value-field="logo_color" - phx-hook="ColorSync" - class="theme-color-row theme-subfield-sm" - > - - {@theme_settings.logo_color} -
- <% end %> -
- <% end %> - <% end %> -
- """ - 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""" -
- Upload header image -
- -
- - <%= if @header_image do %> -
- - -
- - <%= if @contrast_warning != :ok do %> -
- - <%= if @contrast_warning == :poor do %> - Text may be hard to read - <% else %> - Text contrast could be better - <% end %> - -

- Try switching to a - <%= if @theme_settings.mood == "dark" do %> - lighter mood - <% else %> - dark mood - <% end %> - or choosing a different image. -

-
- <% end %> - -
-
"update_setting"} phx-value-field="header_zoom"> -
- Zoom - {@theme_settings.header_zoom}% -
- -
- <%= if @theme_settings.header_zoom > 100 do %> -
"update_setting"} - phx-value-field="header_position_x" - > -
- Horizontal position - {@theme_settings.header_position_x}% -
- -
-
"update_setting"} - phx-value-field="header_position_y" - > -
- Vertical position - {@theme_settings.header_position_y}% -
- -
- <% end %> -
- <% 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 %> -

{error_to_string(err)}

- <% end %> - <% end %> - - <%= for err <- upload_errors(@uploads.theme_header_upload) do %> -

{error_to_string(err)}

- <% end %> -
- """ - end - - # Shared upload progress component - attr :entry, :map, required: true - attr :upload_name, :string, required: true - - defp upload_progress(assigns) do - ~H""" -
-
-
-
- {@entry.progress}% - -
- """ - 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. diff --git a/lib/berrypod_web/page_editor_hook.ex b/lib/berrypod_web/page_editor_hook.ex index 417562b..d38f1ac 100644 --- a/lib/berrypod_web/page_editor_hook.ex +++ b/lib/berrypod_web/page_editor_hook.ex @@ -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} diff --git a/lib/berrypod_web/page_renderer.ex b/lib/berrypod_web/page_renderer.ex index 3eeef5a..bb83188 100644 --- a/lib/berrypod_web/page_renderer.ex +++ b/lib/berrypod_web/page_renderer.ex @@ -226,6 +226,13 @@ defmodule BerrypodWeb.PageRenderer do """ end