defmodule BerrypodWeb.BlockEditorComponents do @moduledoc """ Shared UI components for the block editor. Used by both the admin page editor (`Admin.Pages.Editor`) and the live page editor sidebar (`PageEditorHook` + `PageRenderer`). All components accept an `event_prefix` attr to namespace phx-click events. The admin editor uses `""` (default), the live sidebar uses `"editor_"`. """ use Phoenix.Component import BerrypodWeb.CoreComponents, only: [icon: 1] alias Berrypod.Pages.{BlockEditor, BlockTypes} alias BerrypodWeb.BlockThumbnails # ── Block card ───────────────────────────────────────────────── attr :block, :map, required: true attr :idx, :integer, required: true attr :total, :integer, required: true attr :expanded, :any, required: true attr :event_prefix, :string, default: "" def block_card(assigns) do block_type = BlockTypes.get(assigns.block["type"]) has_settings = BlockEditor.has_settings?(assigns.block) expanded = MapSet.member?(assigns.expanded, assigns.block["id"]) preview = block_preview(assigns.block, block_type) assigns = assigns |> assign(:block_type, block_type) |> assign(:has_settings, has_settings) |> assign(:is_expanded, expanded) |> assign(:preview, preview) ~H"""
{@idx + 1} <.icon name={(@block_type && @block_type.icon) || "hero-puzzle-piece"} class="size-5" />
{(@block_type && @block_type.name) || @block["type"]} {@preview}
<.block_settings_form :if={@is_expanded} block={@block} schema={@block_type.settings_schema} hint={@block_type[:hint]} event_prefix={@event_prefix} />
""" end # ── Settings form ────────────────────────────────────────────── attr :block, :map, required: true attr :schema, :list, required: true attr :hint, :string, default: nil attr :event_prefix, :string, default: "" def block_settings_form(assigns) do settings = BlockEditor.settings_with_defaults(assigns.block) assigns = assign(assigns, :settings, settings) ~H"""

<.icon name="hero-information-circle-mini" class="size-4" /> {@hint}

<.block_field :for={field <- @schema} field={field} value={@settings[field.key]} block_id={@block["id"]} event_prefix={@event_prefix} />
""" end # ── Field renderers ──────────────────────────────────────────── attr :field, :any, required: true attr :value, :any, required: true attr :block_id, :string, required: true attr :event_prefix, :string, default: "" def block_field(%{field: %{type: :select}} = assigns) do ~H"""
""" end def block_field(%{field: %{type: :textarea}} = assigns) do ~H"""
""" end def block_field(%{field: %{type: :number}} = assigns) do ~H"""
""" end def block_field(%{field: %{type: :image}} = assigns) do image_id = assigns.value image = if is_binary(image_id) && image_id != "", do: Berrypod.Media.get_image(image_id) assigns = assign(assigns, :image, image) ~H"""
{@field.label}
<%= if @image do %>
<%= if @image.is_svg do %>
<.icon name="hero-code-bracket" class="size-6" />
<% else %> {@image.alt <% end %>
{@image.filename} {@image.alt}
<% else %> <% end %>
""" end def block_field(%{field: %{type: :repeater}} = assigns) do items = if is_list(assigns.value), do: assigns.value, else: [] item_count = length(items) assigns = assigns |> assign(:items, items) |> assign(:item_count, item_count) ~H"""
{@field.label}
  1. Item {idx + 1}: {item["label"] || item[:label] || "New item"}
    <.repeater_item_field :for={sub_field <- @field.item_schema} sub_field={sub_field} item={item} field_key={@field.key} block_id={@block_id} index={idx} event_prefix={@event_prefix} />
""" end def block_field(assigns) do ~H"""
""" end # ── Repeater item field ──────────────────────────────────────── attr :sub_field, :any, required: true attr :item, :map, required: true attr :field_key, :string, required: true attr :block_id, :string, required: true attr :index, :integer, required: true attr :event_prefix, :string, default: "" def repeater_item_field(assigns) do value = assigns.item[assigns.sub_field.key] || "" assigns = assign(assigns, :value, value) ~H""" """ end # ── Block picker ─────────────────────────────────────────────── attr :allowed_blocks, :any, required: true attr :filter, :string, required: true attr :event_prefix, :string, default: "" def block_picker(assigns) do filter = String.downcase(assigns.filter) filtered = assigns.allowed_blocks |> Enum.filter(fn {_type, def} -> filter == "" or String.contains?(String.downcase(def.name), filter) or String.contains?(String.downcase(Map.get(def, :description, "")), filter) end) |> Enum.sort_by(fn {_type, def} -> def.name end) assigns = assign(assigns, :filtered_blocks, filtered) ~H"""

Add a block

No matching blocks.

""" end # ── Image picker ────────────────────────────────────────────── attr :images, :list, required: true attr :search, :string, required: true attr :event_prefix, :string, default: "" attr :upload, :any, default: nil def image_picker(assigns) do search = String.downcase(assigns.search) filtered = if search == "" do assigns.images else Enum.filter(assigns.images, fn img -> String.contains?(String.downcase(img.filename || ""), search) or String.contains?(String.downcase(img.alt || ""), search) end) end assigns = assign(assigns, :filtered_images, filtered) ~H"""

Choose image

No images found.

""" end # ── Block preview ────────────────────────────────────────────── # Extracts a short one-line preview from block settings for display in the card defp block_preview(_block, nil), do: nil defp block_preview(block, block_type) do settings = block["settings"] || %{} schema = block_type.settings_schema # Try text/textarea fields first, then selects, then repeaters find_text_preview(settings, schema) || find_select_preview(settings, schema) || find_repeater_preview(settings, schema) end defp find_text_preview(settings, schema) do schema |> Enum.filter(&(&1.type in [:text, :textarea])) |> Enum.find_value(fn field -> case settings[field.key] do val when is_binary(val) and val != "" -> truncate(val, 60) _ -> nil end end) end defp find_select_preview(settings, schema) do schema |> Enum.filter(&(&1.type == :select)) |> Enum.find_value(fn field -> case settings[field.key] do val when is_binary(val) and val != "" -> "#{field.label}: #{val}" _ -> nil end end) end defp find_repeater_preview(settings, schema) do schema |> Enum.filter(&(&1.type == :repeater)) |> Enum.find_value(fn field -> case settings[field.key] do items when is_list(items) and items != [] -> count = length(items) "#{count} #{if count == 1, do: "item", else: "items"}" _ -> nil end end) end defp truncate(str, max) do str = String.replace(str, ~r/\s+/, " ") |> String.trim() if String.length(str) > max do String.slice(str, 0, max) <> "..." else str end end end