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} # ── 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"]) assigns = assigns |> assign(:block_type, block_type) |> assign(:has_settings, has_settings) |> assign(:is_expanded, expanded) ~H"""
{@idx + 1} <.icon name={(@block_type && @block_type.icon) || "hero-puzzle-piece"} class="size-5" /> {(@block_type && @block_type.name) || @block["type"]}
<.block_settings_form :if={@is_expanded} block={@block} schema={@block_type.settings_schema} event_prefix={@event_prefix} />
""" end # ── Settings form ────────────────────────────────────────────── attr :block, :map, required: true attr :schema, :list, required: true attr :event_prefix, :string, default: "" def block_settings_form(assigns) do settings = BlockEditor.settings_with_defaults(assigns.block) assigns = assign(assigns, :settings, settings) ~H"""
<.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 %>
<.icon name="hero-photo" class="size-6" /> No image selected
<% 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) end) |> Enum.sort_by(fn {_type, def} -> def.name end) assigns = assign(assigns, :filtered_blocks, filtered) ~H"""

Add a block

No matching blocks.

""" end end