diff --git a/PROGRESS.md b/PROGRESS.md
index 9d994fc..ce90b26 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -92,8 +92,8 @@ Extend the existing page editor (PageEditorHook + editor_sheet) to include theme
| 3 | Extract theme editor into reusable component | 3h | done |
| 3b | Create settings editor component | 2h | done |
| 4 | Image upload handling in hook context | 2h | done |
-| 5 | URL-based mode activation (?edit=theme) | 1h | planned |
-| 6 | Admin routing redirect | 30m | planned |
+| 5 | URL-based mode activation (?edit=theme) | 1h | done |
+| 6 | Admin routing redirect | 30m | done |
| 7 | Polish and testing | 2h | planned |
### Quick fixes (from usability testing)
diff --git a/lib/berrypod_web/components/layouts/admin.html.heex b/lib/berrypod_web/components/layouts/admin.html.heex
index b6db09e..c340895 100644
--- a/lib/berrypod_web/components/layouts/admin.html.heex
+++ b/lib/berrypod_web/components/layouts/admin.html.heex
@@ -143,10 +143,7 @@
- <.link
- href={~p"/admin/theme"}
- class={admin_nav_active?(@current_path, "/admin/theme")}
- >
+ <.link href="/?edit=theme">
<.icon name="hero-paint-brush" class="size-5" /> Theme
diff --git a/lib/berrypod_web/components/shop_components/theme_editor.ex b/lib/berrypod_web/components/shop_components/theme_editor.ex
index 769981c..b153bb9 100644
--- a/lib/berrypod_web/components/shop_components/theme_editor.ex
+++ b/lib/berrypod_web/components/shop_components/theme_editor.ex
@@ -1,8 +1,6 @@
defmodule BerrypodWeb.ShopComponents.ThemeEditor do
@moduledoc """
- Shared theme editor components used in both:
- - Admin theme page (`/admin/theme`)
- - On-site editor panel (page editor Theme tab)
+ Theme editor components for the on-site editor panel (page editor Theme tab).
Components render settings controls that emit standard events:
- `update_setting` / `theme_update_setting` (phx-click/phx-change)
diff --git a/lib/berrypod_web/controllers/redirect_controller.ex b/lib/berrypod_web/controllers/redirect_controller.ex
new file mode 100644
index 0000000..54f3b4c
--- /dev/null
+++ b/lib/berrypod_web/controllers/redirect_controller.ex
@@ -0,0 +1,10 @@
+defmodule BerrypodWeb.RedirectController do
+ use BerrypodWeb, :controller
+
+ @doc """
+ Redirects /admin/theme to the on-site theme editor.
+ """
+ def theme(conn, _params) do
+ redirect(conn, to: "/?edit=theme")
+ end
+end
diff --git a/lib/berrypod_web/live/admin/dashboard.ex b/lib/berrypod_web/live/admin/dashboard.ex
index 095e62a..98aed61 100644
--- a/lib/berrypod_web/live/admin/dashboard.ex
+++ b/lib/berrypod_web/live/admin/dashboard.ex
@@ -58,7 +58,7 @@ defmodule BerrypodWeb.Admin.Dashboard do
<.link href={~p"/"} class="admin-btn admin-btn-primary">
<.icon name="hero-arrow-top-right-on-square-mini" class="size-4" /> View your shop
- <.link navigate={~p"/admin/theme"} class="admin-btn admin-btn-secondary">
+ <.link href="/?edit=theme" class="admin-btn admin-btn-secondary">
<.icon name="hero-paint-brush-mini" class="size-4" /> Customise theme
@@ -273,7 +273,7 @@ defmodule BerrypodWeb.Admin.Dashboard do
%{
key: :theme_customised,
label: "Customise your theme",
- href: "/admin/theme?from=checklist",
+ href: "/?edit=theme",
hint: "Upload your logo, pick your colours, and choose a font that matches your brand."
},
%{
diff --git a/lib/berrypod_web/live/admin/theme/index.ex b/lib/berrypod_web/live/admin/theme/index.ex
deleted file mode 100644
index c893484..0000000
--- a/lib/berrypod_web/live/admin/theme/index.ex
+++ /dev/null
@@ -1,630 +0,0 @@
-defmodule BerrypodWeb.Admin.Theme.Index do
- use BerrypodWeb, :live_view
-
- alias Berrypod.{Pages, Settings}
- alias Berrypod.Media
- alias Berrypod.Theme.{Contrast, CSSGenerator, Presets, PreviewData}
- alias Berrypod.Workers.FaviconGeneratorWorker
-
- @impl true
- def mount(_params, _session, socket) do
- theme_settings = Settings.get_theme_settings()
- generated_css = CSSGenerator.generate(theme_settings)
- active_preset = Presets.detect_preset(theme_settings)
-
- preview_data = %{
- products: PreviewData.products(),
- cart_items: PreviewData.cart_items(),
- testimonials: PreviewData.testimonials(),
- categories: PreviewData.categories()
- }
-
- logo_image = Media.get_logo()
- header_image = Media.get_header()
- icon_image = Media.get_icon()
-
- socket =
- socket
- |> assign(:theme_settings, theme_settings)
- |> assign(:site_name, Settings.site_name())
- |> assign(:site_description, Settings.site_description())
- |> assign(:generated_css, generated_css)
- |> assign(:preview_page, :home)
- |> assign(:presets_with_descriptions, Presets.all_with_descriptions())
- |> assign(:active_preset, active_preset)
- |> assign(:preview_data, preview_data)
- |> assign(:logo_image, logo_image)
- |> assign(:header_image, header_image)
- |> assign(:icon_image, icon_image)
- |> assign(:customise_open, false)
- |> assign(:sidebar_collapsed, false)
- |> assign(:cart_drawer_open, false)
- |> compute_header_contrast_warning()
- |> allow_upload(:logo_upload,
- accept: ~w(.png .jpg .jpeg .webp .svg),
- max_entries: 1,
- max_file_size: 2_000_000,
- auto_upload: true,
- progress: &handle_progress/3
- )
- |> allow_upload(:header_upload,
- accept: ~w(.png .jpg .jpeg .webp),
- max_entries: 1,
- max_file_size: 5_000_000,
- auto_upload: true,
- progress: &handle_progress/3
- )
- |> allow_upload(:icon_upload,
- accept: ~w(.png .jpg .jpeg .webp .svg),
- max_entries: 1,
- max_file_size: 5_000_000,
- auto_upload: true,
- progress: &handle_progress/3
- )
-
- {:ok, assign(socket, :from_checklist, false)}
- end
-
- @impl true
- def handle_params(params, _uri, socket) do
- {:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
- end
-
- defp handle_progress(:logo_upload, entry, socket) do
- if entry.done? do
- consume_uploaded_entries(socket, :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_settings.use_logo_as_icon do
- enqueue_favicon_generation(image.id)
- end
-
- {:noreply, assign(socket, :logo_image, image)}
-
- _ ->
- {:noreply, socket}
- end
- else
- {:noreply, socket}
- end
- end
-
- defp handle_progress(:header_upload, entry, socket) do
- if entry.done? do
- consume_uploaded_entries(socket, :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(:header_image, image)
- |> compute_header_contrast_warning()
-
- {:noreply, socket}
-
- _ ->
- {:noreply, socket}
- end
- else
- {:noreply, socket}
- end
- end
-
- defp handle_progress(:icon_upload, entry, socket) do
- if entry.done? do
- consume_uploaded_entries(socket, :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, :icon_image, image)}
-
- _ ->
- {:noreply, socket}
- end
- else
- {:noreply, socket}
- end
- end
-
- @impl true
- def handle_event("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} ->
- generated_css = CSSGenerator.generate(theme_settings)
-
- socket =
- socket
- |> assign(:theme_settings, theme_settings)
- |> assign(:generated_css, generated_css)
- |> assign(:active_preset, preset_atom)
- |> compute_header_contrast_warning()
- |> put_flash(:info, "Applied #{preset_name} preset")
-
- {:noreply, socket}
-
- {:error, _} ->
- {:noreply, put_flash(socket, :error, "Failed to apply preset")}
- end
- end
-
- @impl true
- def handle_event("change_preview_page", %{"page" => page_name}, socket) do
- page_atom = String.to_existing_atom(page_name)
-
- socket =
- socket
- |> assign(:preview_page, page_atom)
- |> push_event("scroll-preview-top", %{})
-
- {:noreply, socket}
- end
-
- # Settings stored outside the theme JSON
- @standalone_settings ~w(site_name site_description)
-
- @impl true
- def handle_event("update_setting", %{"field" => field, "setting_value" => value}, socket)
- when field in @standalone_settings do
- Settings.put_setting(field, value, "string")
- {:noreply, assign(socket, String.to_existing_atom(field), value)}
- end
-
- @impl true
- def handle_event("update_setting", %{"field" => field, "setting_value" => value}, socket) do
- field_atom = String.to_existing_atom(field)
- attrs = %{field_atom => value}
-
- case Settings.update_theme_settings(attrs) do
- {:ok, theme_settings} ->
- generated_css = CSSGenerator.generate(theme_settings)
- active_preset = Presets.detect_preset(theme_settings)
-
- socket =
- socket
- |> assign(:theme_settings, theme_settings)
- |> assign(:generated_css, generated_css)
- |> assign(:active_preset, active_preset)
- |> maybe_recompute_contrast_warning(field)
-
- {:noreply, socket}
-
- {:error, _} ->
- {:noreply, socket}
- end
- end
-
- @impl true
- def handle_event("update_setting", %{"field" => field} = params, socket)
- when field in @standalone_settings do
- value = params[field]
-
- if value do
- Settings.put_setting(field, value, "string")
- {:noreply, assign(socket, String.to_existing_atom(field), value)}
- else
- {:noreply, socket}
- end
- end
-
- @impl true
- def handle_event("update_setting", %{"field" => field} = params, socket) do
- # For phx-change events from select/input elements, the value comes from the name attribute
- value = params[field] || params["#{field}_text"] || params["value"]
-
- if value do
- field_atom = String.to_existing_atom(field)
- attrs = %{field_atom => value}
-
- case Settings.update_theme_settings(attrs) do
- {:ok, theme_settings} ->
- generated_css = CSSGenerator.generate(theme_settings)
- active_preset = Presets.detect_preset(theme_settings)
-
- socket =
- socket
- |> assign(:theme_settings, theme_settings)
- |> assign(:generated_css, generated_css)
- |> assign(:active_preset, active_preset)
- |> maybe_recompute_contrast_warning(field)
-
- {:noreply, socket}
-
- {:error, _} ->
- {:noreply, socket}
- end
- else
- {:noreply, socket}
- end
- end
-
- @impl true
- def handle_event("update_color", %{"field" => field, "value" => value}, socket) do
- field_atom = String.to_existing_atom(field)
- attrs = %{field_atom => value}
-
- case Settings.update_theme_settings(attrs) do
- {:ok, theme_settings} ->
- generated_css = CSSGenerator.generate(theme_settings)
- active_preset = Presets.detect_preset(theme_settings)
-
- socket =
- socket
- |> assign(:theme_settings, theme_settings)
- |> assign(:generated_css, generated_css)
- |> assign(:active_preset, active_preset)
-
- {:noreply, socket}
-
- {:error, _} ->
- {:noreply, socket}
- end
- end
-
- @impl true
- def handle_event("toggle_setting", %{"field" => field}, socket) do
- field_atom = String.to_existing_atom(field)
- current_value = Map.get(socket.assigns.theme_settings, field_atom)
- new_value = !current_value
-
- # Prevent turning off show_site_name when there's no logo to display
- if field_atom == :show_site_name && new_value == false && !has_valid_logo?(socket) do
- {:noreply, put_flash(socket, :error, "Upload a logo first to hide the shop name")}
- else
- attrs = %{field_atom => new_value}
-
- case Settings.update_theme_settings(attrs) do
- {:ok, theme_settings} ->
- generated_css = CSSGenerator.generate(theme_settings)
- active_preset = Presets.detect_preset(theme_settings)
-
- # Trigger favicon regeneration when the icon source changes
- if field_atom == :use_logo_as_icon do
- maybe_enqueue_favicon_from_settings(theme_settings, socket.assigns)
- end
-
- socket =
- socket
- |> assign(:theme_settings, theme_settings)
- |> assign(:generated_css, generated_css)
- |> assign(:active_preset, active_preset)
- |> maybe_recompute_contrast_warning(field)
-
- {:noreply, socket}
-
- {:error, _} ->
- {:noreply, socket}
- end
- end
- end
-
- @impl true
- def handle_event("save_theme", _params, socket) do
- socket = put_flash(socket, :info, "Theme saved successfully")
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("update_image_alt", %{"image-id" => image_id, "alt" => alt}, socket) do
- case Media.get_image(image_id) do
- nil ->
- {:noreply, socket}
-
- image ->
- {:ok, updated} = Media.update_image_metadata(image, %{alt: alt})
-
- # Refresh the relevant assign so the template sees the new alt text
- socket =
- cond do
- socket.assigns.logo_image && socket.assigns.logo_image.id == image_id ->
- assign(socket, :logo_image, updated)
-
- socket.assigns.header_image && socket.assigns.header_image.id == image_id ->
- assign(socket, :header_image, updated)
-
- socket.assigns[:icon_image] && socket.assigns.icon_image &&
- socket.assigns.icon_image.id == image_id ->
- assign(socket, :icon_image, updated)
-
- true ->
- socket
- end
-
- {:noreply, socket}
- end
- end
-
- @impl true
- def handle_event("remove_logo", _params, socket) do
- if logo = socket.assigns.logo_image do
- Media.delete_image(logo)
- end
-
- # Re-enable shop name when removing logo to ensure header isn't empty
- {:ok, theme_settings} =
- Settings.update_theme_settings(%{logo_image_id: nil, show_site_name: true})
-
- generated_css = CSSGenerator.generate(theme_settings)
-
- socket =
- socket
- |> assign(:logo_image, nil)
- |> assign(:theme_settings, theme_settings)
- |> assign(:generated_css, generated_css)
- |> put_flash(:info, "Logo removed")
-
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("remove_header", _params, socket) do
- if header = socket.assigns.header_image do
- Media.delete_image(header)
- end
-
- Settings.update_theme_settings(%{header_image_id: nil})
-
- socket =
- socket
- |> assign(:header_image, nil)
- |> assign(:header_contrast_warning, :ok)
- |> put_flash(:info, "Header image removed")
-
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("remove_icon", _params, socket) do
- if icon = socket.assigns.icon_image do
- Media.delete_image(icon)
- end
-
- Settings.update_theme_settings(%{icon_image_id: nil})
-
- socket =
- socket
- |> assign(:icon_image, nil)
- |> put_flash(:info, "Icon removed")
-
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("cancel_upload", %{"ref" => ref, "upload" => upload_name}, socket) do
- upload_atom = String.to_existing_atom(upload_name)
- {:noreply, cancel_upload(socket, upload_atom, ref)}
- end
-
- @impl true
- def handle_event("toggle_customise", _params, socket) do
- {:noreply, assign(socket, :customise_open, !socket.assigns.customise_open)}
- end
-
- @impl true
- def handle_event("toggle_sidebar", _params, socket) do
- {:noreply, assign(socket, :sidebar_collapsed, !socket.assigns.sidebar_collapsed)}
- end
-
- @impl true
- def handle_event("open_cart_drawer", _params, socket) do
- {:noreply, assign(socket, :cart_drawer_open, true)}
- end
-
- @impl true
- def handle_event("close_cart_drawer", _params, socket) do
- {:noreply, assign(socket, :cart_drawer_open, false)}
- end
-
- @impl true
- def handle_event("noop", _params, socket) do
- {:noreply, socket}
- end
-
- defp has_valid_logo?(socket) do
- socket.assigns.theme_settings.show_logo && socket.assigns.logo_image != nil
- end
-
- def error_to_string(:too_large), do: "File is too large"
- def error_to_string(:too_many_files), do: "Too many files"
- def error_to_string(:not_accepted), do: "File type not accepted"
- def error_to_string(err), do: inspect(err)
-
- defp enqueue_favicon_generation(source_image_id) do
- %{source_image_id: source_image_id}
- |> FaviconGeneratorWorker.new()
- |> Oban.insert()
- end
-
- defp maybe_enqueue_favicon_from_settings(theme_settings, assigns) do
- source_id =
- if theme_settings.use_logo_as_icon do
- case assigns.logo_image do
- %{id: id} -> id
- _ -> nil
- end
- else
- case assigns.icon_image do
- %{id: id} -> id
- _ -> nil
- end
- end
-
- if source_id, do: enqueue_favicon_generation(source_id)
- end
-
- defp preview_assigns(assigns) do
- assign(assigns, %{
- mode: :preview,
- products: assigns.preview_data.products,
- categories: assigns.preview_data.categories,
- cart_items: PreviewData.cart_drawer_items(),
- cart_count: 2,
- cart_subtotal: "£72.00",
- header_nav_items: BerrypodWeb.ThemeHook.default_header_nav(),
- footer_nav_items: BerrypodWeb.ThemeHook.default_footer_nav()
- })
- end
-
- # Unified preview — loads page definition, applies context, renders via PageRenderer
- attr :page, :atom, required: true
- attr :preview_data, :map, required: true
- attr :theme_settings, :map, required: true
- attr :site_name, :string, required: true
- attr :logo_image, :any, required: true
- attr :header_image, :any, required: true
- attr :cart_drawer_open, :boolean, default: false
-
- defp preview_page(assigns) do
- slug = to_string(assigns.page)
- page = Pages.get_page(slug)
-
- assigns =
- assigns
- |> preview_assigns()
- |> assign(:page, page)
- |> preview_page_context(slug)
-
- extra = Pages.load_block_data(page.blocks, assigns)
- assigns = assign(assigns, extra)
-
- ~H" "
- end
-
- # Page-context data needed by specific page types in preview mode
- defp preview_page_context(assigns, "pdp") do
- product = List.first(assigns.preview_data.products)
- option_types = Map.get(product, :option_types) || []
- variants = Map.get(product, :variants) || []
-
- {selected_options, selected_variant} =
- case variants do
- [first | _] -> {first.options, first}
- [] -> {%{}, nil}
- end
-
- available_options =
- Enum.reduce(option_types, %{}, fn opt, acc ->
- values = Enum.map(opt.values, & &1.title)
- Map.put(acc, opt.name, values)
- end)
-
- display_price =
- if selected_variant, do: selected_variant.price, else: product.cheapest_price
-
- assigns
- |> assign(:product, product)
- |> assign(:gallery_images, build_gallery_images(product))
- |> assign(:option_types, option_types)
- |> assign(:selected_options, selected_options)
- |> assign(:available_options, available_options)
- |> assign(:display_price, display_price)
- |> assign(:quantity, 1)
- |> assign(:option_urls, %{})
- end
-
- defp preview_page_context(assigns, "cart") do
- cart_items = assigns.preview_data.cart_items
-
- subtotal =
- Enum.reduce(cart_items, 0, fn item, acc ->
- acc + item.product.cheapest_price * item.quantity
- end)
-
- assigns
- |> assign(:cart_page_items, cart_items)
- |> assign(:cart_page_subtotal, subtotal)
- end
-
- defp preview_page_context(assigns, "about") do
- assign(assigns, :content_blocks, PreviewData.about_content())
- end
-
- defp preview_page_context(assigns, "delivery") do
- assign(assigns, :content_blocks, PreviewData.delivery_content())
- end
-
- defp preview_page_context(assigns, "privacy") do
- assign(assigns, :content_blocks, PreviewData.privacy_content())
- end
-
- defp preview_page_context(assigns, "terms") do
- assign(assigns, :content_blocks, PreviewData.terms_content())
- end
-
- defp preview_page_context(assigns, "error") do
- assign(assigns, %{
- error_code: "404",
- error_title: "Page Not Found",
- error_description:
- "Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved."
- })
- end
-
- defp preview_page_context(assigns, _slug), do: assigns
-
- defp build_gallery_images(product) do
- alias Berrypod.Products.ProductImage
-
- (Map.get(product, :images) || [])
- |> Enum.sort_by(& &1.position)
- |> Enum.map(fn img -> ProductImage.url(img, 1200) end)
- |> Enum.reject(&is_nil/1)
- |> case do
- [] -> []
- urls -> urls
- end
- end
-
- # Compute header contrast warning based on image colors and theme mood
- defp compute_header_contrast_warning(socket) do
- header_image = socket.assigns.header_image
- theme_settings = socket.assigns.theme_settings
-
- warning =
- if theme_settings.header_background_enabled && header_image do
- text_color = Contrast.text_color_for_mood(theme_settings.mood)
- colors = Contrast.parse_dominant_colors(header_image.dominant_colors)
- Contrast.analyze_header_contrast(colors, text_color)
- else
- :ok
- end
-
- assign(socket, :header_contrast_warning, warning)
- end
-
- # Only recompute when mood or header_background_enabled changes
- defp maybe_recompute_contrast_warning(socket, field)
- when field in ["mood", "header_background_enabled"] do
- compute_header_contrast_warning(socket)
- end
-
- defp maybe_recompute_contrast_warning(socket, _field), do: socket
-end
diff --git a/lib/berrypod_web/live/admin/theme/index.html.heex b/lib/berrypod_web/live/admin/theme/index.html.heex
deleted file mode 100644
index 3900a38..0000000
--- a/lib/berrypod_web/live/admin/theme/index.html.heex
+++ /dev/null
@@ -1,651 +0,0 @@
-
-
-
-
-
-
-
-
-
- <%= for {page_name, label} <- [
- {:home, "Home"},
- {:collection, "Collection"},
- {:pdp, "Product"},
- {:cart, "Cart"},
- {:about, "About"},
- {:delivery, "Delivery"},
- {:privacy, "Privacy"},
- {:terms, "Terms"},
- {:contact, "Contact"},
- {:error, "404"}
- ] do %>
-
- {label}
-
- <% end %>
-
-
-
-
-
-
-
-
-
-
-
- {@site_name |> String.downcase() |> String.replace(" ", "")}.myshopify.com
-
-
-
-
-
-
-
-
- <.preview_page
- page={@preview_page}
- preview_data={@preview_data}
- theme_settings={@theme_settings}
- site_name={@site_name}
- logo_image={@logo_image}
- header_image={@header_image}
- cart_drawer_open={@cart_drawer_open}
- />
-
-
-
-
diff --git a/lib/berrypod_web/router.ex b/lib/berrypod_web/router.ex
index 00f3837..34099be 100644
--- a/lib/berrypod_web/router.ex
+++ b/lib/berrypod_web/router.ex
@@ -183,11 +183,8 @@ defmodule BerrypodWeb.Router do
live "/redirects", Admin.Redirects, :index
end
- # Theme editor: admin root layout but full-screen (no sidebar)
- live_session :admin_theme,
- on_mount: [{BerrypodWeb.UserAuth, :require_authenticated}] do
- live "/theme", Admin.Theme.Index, :index
- end
+ # Theme editor redirects to on-site editing
+ get "/theme", RedirectController, :theme
end
# User account settings
diff --git a/test/berrypod_web/live/admin/layout_test.exs b/test/berrypod_web/live/admin/layout_test.exs
index ae266ac..bc80c21 100644
--- a/test/berrypod_web/live/admin/layout_test.exs
+++ b/test/berrypod_web/live/admin/layout_test.exs
@@ -19,7 +19,7 @@ defmodule BerrypodWeb.Admin.LayoutTest do
assert has_element?(view, ~s(a[href="/admin"]), "Dashboard")
assert has_element?(view, ~s(a[href="/admin/orders"]), "Orders")
- assert has_element?(view, ~s(a[href="/admin/theme"]), "Theme")
+ assert has_element?(view, ~s(a[href="/?edit=theme"]), "Theme")
assert has_element?(view, ~s(a[href="/admin/settings"]), "Settings")
end
@@ -45,24 +45,6 @@ defmodule BerrypodWeb.Admin.LayoutTest do
end
end
- describe "theme editor layout" do
- setup %{conn: conn, user: user} do
- %{conn: log_in_user(conn, user)}
- end
-
- test "does not render sidebar", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/admin/theme")
-
- refute html =~ "admin-drawer"
- end
-
- test "shows back link to admin", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- assert has_element?(view, ~s(a[href="/admin"]), "Admin")
- end
- end
-
describe "admin bar on shop pages" do
setup do
{:ok, _} = Berrypod.Settings.set_site_live(true)
diff --git a/test/berrypod_web/live/admin/theme_test.exs b/test/berrypod_web/live/admin/theme_test.exs
index 85eb5ac..696099a 100644
--- a/test/berrypod_web/live/admin/theme_test.exs
+++ b/test/berrypod_web/live/admin/theme_test.exs
@@ -1,216 +1,24 @@
defmodule BerrypodWeb.Admin.ThemeTest do
use BerrypodWeb.ConnCase, async: false
- import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
- alias Berrypod.Settings
-
setup do
user = user_fixture()
%{user: user}
end
- describe "Index (unauthenticated)" do
- test "redirects to login when not authenticated", %{conn: conn} do
- {:error, redirect} = live(conn, ~p"/admin/theme")
+ describe "/admin/theme redirect" do
+ test "redirects unauthenticated users to login", %{conn: conn} do
+ conn = get(conn, ~p"/admin/theme")
- assert {:redirect, %{to: path}} = redirect
- assert path == ~p"/users/log-in"
- end
- end
-
- describe "Index (authenticated)" do
- setup %{conn: conn, user: user} do
- conn = log_in_user(conn, user)
- %{conn: conn}
+ assert redirected_to(conn) == ~p"/users/log-in"
end
- test "renders theme editor page", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/admin/theme")
+ test "redirects authenticated users to on-site editor", %{conn: conn, user: user} do
+ conn = conn |> log_in_user(user) |> get(~p"/admin/theme")
- assert html =~ "Theme "
- assert html =~ "preset"
- end
-
- test "displays all 8 presets", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/admin/theme")
-
- assert html =~ "gallery"
- assert html =~ "studio"
- assert html =~ "boutique"
- assert html =~ "bold"
- assert html =~ "playful"
- assert html =~ "minimal"
- assert html =~ "night"
- assert html =~ "classic"
- end
-
- test "displays current theme settings", %{conn: conn} do
- {:ok, _settings} = Settings.apply_preset(:gallery)
- {:ok, _view, html} = live(conn, ~p"/admin/theme")
-
- assert html =~ "warm"
- assert html =~ "editorial"
- assert html =~ "soft"
- assert html =~ "spacious"
- end
-
- test "displays generated CSS in preview", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/admin/theme")
-
- # CSS generator outputs accent colors and layout variables for shop pages
- assert html =~ ".themed {"
- assert html =~ "--t-accent-h:"
- end
-
- test "applies preset and updates preview", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- html =
- view
- |> element("button", "gallery")
- |> render_click()
-
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.mood == "warm"
- assert theme_settings.typography == "editorial"
- assert html =~ "warm"
- assert html =~ "editorial"
- end
-
- test "switches preview page", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- html =
- view
- |> element("button", "Collection")
- |> render_click()
-
- assert html =~ "All Products"
- end
-
- test "theme settings are saved when applying a preset", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- # Apply a preset
- view
- |> element("button", "gallery")
- |> render_click()
-
- # Verify settings were persisted
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.mood == "warm"
- assert theme_settings.typography == "editorial"
- end
-
- test "all preview page buttons are present", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/admin/theme")
-
- assert html =~ "Home"
- assert html =~ "Collection"
- assert html =~ "Product"
- assert html =~ "Cart"
- assert html =~ "About"
- assert html =~ "Contact"
- assert html =~ "404"
- end
-
- test "mood customization buttons work", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- # Click the "dark" mood button
- html =
- view
- |> element("button[phx-value-setting_value='dark']")
- |> render_click()
-
- # Verify the setting was updated
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.mood == "dark"
- assert html =~ "dark"
- end
-
- test "shape customization buttons work", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- # Click the "round" shape button
- view
- |> element("button[phx-value-setting_value='round']")
- |> render_click()
-
- # Verify the setting was updated
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.shape == "round"
- end
-
- test "density customization buttons work", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- # Click the "compact" density button
- view
- |> element("button[phx-value-setting_value='compact']")
- |> render_click()
-
- # Verify the setting was updated
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.density == "compact"
- end
-
- test "grid columns customization buttons work", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- # Click the "2 columns" grid columns button
- view
- |> element("button", "2 columns")
- |> render_click()
-
- # Verify the setting was updated
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.grid_columns == "2"
- end
-
- test "typography customization buttons work", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- # Click the "modern" typography button
- view
- |> element("button[phx-value-setting_value='modern']")
- |> render_click()
-
- # Verify the setting was updated
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.typography == "modern"
- end
-
- test "header layout customization buttons work", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/admin/theme")
-
- # Click the "centered" header layout button
- view
- |> element("button[phx-value-setting_value='centered']")
- |> render_click()
-
- # Verify the setting was updated
- theme_settings = Settings.get_theme_settings()
- assert theme_settings.header_layout == "centered"
- end
-
- test "CSS regenerates when settings change", %{conn: conn} do
- {:ok, view, html} = live(conn, ~p"/admin/theme")
-
- # Capture initial CSS
- initial_css = html
-
- # Change a setting
- new_html =
- view
- |> element("button[phx-value-setting_value='dark']")
- |> render_click()
-
- # Verify CSS has changed
- refute initial_css == new_html
- assert new_html =~ "--t-accent-h:"
+ assert redirected_to(conn) == "/?edit=theme"
end
end
end
diff --git a/test/berrypod_web/live/theme_css_consistency_test.exs b/test/berrypod_web/live/theme_css_consistency_test.exs
index b66f470..a2605a8 100644
--- a/test/berrypod_web/live/theme_css_consistency_test.exs
+++ b/test/berrypod_web/live/theme_css_consistency_test.exs
@@ -1,12 +1,11 @@
defmodule BerrypodWeb.ThemeCSSConsistencyTest do
@moduledoc """
- Tests that verify CSS works correctly for both the theme editor
- preview and the shop pages using the shared .themed class.
+ Tests that verify CSS works correctly for shop pages using the .themed class.
Architecture:
- - Both shop pages and preview use .themed class for shared styles
- - Theme editor uses .preview-frame[data-*] selectors for live switching (in admin.css)
- - Shop pages get theme values via inline CSS from CSSGenerator (shop.css)
+ - Shop pages use .themed class for theme-aware styles
+ - Theme editor is on-site (/?edit=theme) so it uses the same CSS as shop pages
+ - Shop pages get theme values via inline CSS from CSSGenerator
- Component styles use .themed for shared styling (theme-layer2-attributes.css)
"""
@@ -34,11 +33,12 @@ defmodule BerrypodWeb.ThemeCSSConsistencyTest do
assert html =~ ~r/data-density="/
end
- test "theme editor has .themed with data attributes", %{conn: conn, user: user} do
+ test "on-site theme editor has .themed with data attributes", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
- {:ok, _view, html} = live(conn, ~p"/admin/theme")
+ # Theme editor is now on-site at /?edit=theme
+ {:ok, _view, html} = live(conn, ~p"/?edit=theme")
- # Verify themed element exists in preview-frame with theme data attributes
+ # Verify themed element exists with theme data attributes
assert html =~ ~r/]*class="themed/
assert html =~ ~r/data-mood="/
assert html =~ ~r/data-typography="/
@@ -46,26 +46,6 @@ defmodule BerrypodWeb.ThemeCSSConsistencyTest do
assert html =~ ~r/data-density="/
end
- test "shop page uses same theme settings as preview", %{conn: conn, user: user} do
- # Set a specific theme configuration
- {:ok, _settings} = Settings.apply_preset(:night)
-
- # Check shop page (logged in since site_live is false by default)
- conn = log_in_user(conn, user)
- {:ok, _view, shop_html} = live(conn, ~p"/")
-
- # Check preview (already authenticated)
- {:ok, _view, preview_html} = live(conn, ~p"/admin/theme")
-
- # Extract data-mood values from both
- [_, shop_mood] = Regex.run(~r/data-mood="([^"]+)"/, shop_html)
- [_, preview_mood] = Regex.run(~r/data-mood="([^"]+)"/, preview_html)
-
- # They should match
- assert shop_mood == preview_mood
- assert shop_mood == "dark"
- end
-
test "theme settings changes are reflected on shop page", %{conn: conn, user: user} do
conn = log_in_user(conn, user)