diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css index 76c63a9..df00da7 100644 --- a/assets/css/admin/components.css +++ b/assets/css/admin/components.css @@ -1453,11 +1453,23 @@ flex-shrink: 0; } -.block-card-name { +.block-card-info { flex: 1; + min-width: 0; +} + +.block-card-name { font-size: 0.875rem; font-weight: 500; - min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.block-card-preview { + display: block; + font-size: 0.75rem; + color: color-mix(in oklch, var(--t-text-primary) 50%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -1567,13 +1579,25 @@ } } +.block-picker-thumb { + grid-column: 1 / -1; + width: 100%; + height: auto; + border-radius: 0.25rem; + background: color-mix(in oklch, var(--t-text-primary) 3%, transparent); + margin-bottom: 0.25rem; +} + .block-picker-item > .size-5 { - grid-row: 1 / -1; margin-top: 0.0625rem; } -.block-picker-item-desc { +.block-picker-item-name { grid-column: 2; +} + +.block-picker-item-desc { + grid-column: 1 / -1; font-size: 0.6875rem; line-height: 1.3; color: color-mix(in oklch, var(--t-text-primary) 55%, transparent); @@ -1606,6 +1630,26 @@ /* Block settings panel */ +.block-settings-hint { + display: flex; + align-items: flex-start; + gap: 0.375rem; + font-size: 0.75rem; + color: color-mix(in oklch, var(--t-text-primary) 60%, transparent); + background: color-mix(in oklch, var(--t-accent) 8%, transparent); + border: 1px solid color-mix(in oklch, var(--t-accent) 20%, transparent); + border-radius: 0.375rem; + padding: 0.5rem 0.625rem; + margin-bottom: 0.5rem; + line-height: 1.4; +} + +.block-settings-hint svg { + flex-shrink: 0; + color: var(--t-accent); + margin-top: 0.05rem; +} + .block-card-settings { padding: 0.75rem 0.75rem 0.25rem; padding-left: 2.75rem; diff --git a/lib/berrypod/pages/block_types.ex b/lib/berrypod/pages/block_types.ex index 83a40b9..b266b66 100644 --- a/lib/berrypod/pages/block_types.ex +++ b/lib/berrypod/pages/block_types.ex @@ -110,10 +110,27 @@ defmodule Berrypod.Pages.BlockTypes do }, "newsletter_card" => %{ name: "Newsletter signup", - description: "Email signup form for collecting subscriber addresses", + description: "Email signup form — currently decorative, does not collect emails", icon: "hero-envelope", allowed_on: :all, - settings_schema: [] + hint: + "This block is decorative — form submissions aren't collected yet. Use it as a placeholder or remove it.", + settings_schema: [ + %SettingsField{key: "title", label: "Title", type: :text, default: "Newsletter"}, + %SettingsField{ + key: "description", + label: "Description", + type: :textarea, + default: + "Sample newsletter signup. Replace with your own message to encourage subscribers." + }, + %SettingsField{ + key: "button_text", + label: "Button text", + type: :text, + default: "Subscribe" + } + ] }, "social_links_card" => %{ name: "Social links", diff --git a/lib/berrypod_web/components/block_editor_components.ex b/lib/berrypod_web/components/block_editor_components.ex index eb14a2d..f851233 100644 --- a/lib/berrypod_web/components/block_editor_components.ex +++ b/lib/berrypod_web/components/block_editor_components.ex @@ -15,6 +15,7 @@ defmodule BerrypodWeb.BlockEditorComponents do import BerrypodWeb.CoreComponents, only: [icon: 1] alias Berrypod.Pages.{BlockEditor, BlockTypes} + alias BerrypodWeb.BlockThumbnails # ── Block card ───────────────────────────────────────────────── @@ -28,12 +29,14 @@ defmodule BerrypodWeb.BlockEditorComponents 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"""
- - {(@block_type && @block_type.name) || @block["type"]} - +
+ + {(@block_type && @block_type.name) || @block["type"]} + + {@preview} +
@@ -121,6 +128,7 @@ defmodule BerrypodWeb.BlockEditorComponents do 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 @@ -129,6 +137,9 @@ defmodule BerrypodWeb.BlockEditorComponents do ~H"""
+

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

@@ -454,6 +465,7 @@ defmodule BerrypodWeb.BlockEditorComponents do phx-value-type={type} class="block-picker-item" > + <.icon name={def.icon} class="size-5" /> {def.name} @@ -564,4 +576,69 @@ defmodule BerrypodWeb.BlockEditorComponents do
""" 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 diff --git a/lib/berrypod_web/components/block_thumbnails.ex b/lib/berrypod_web/components/block_thumbnails.ex new file mode 100644 index 0000000..3519cee --- /dev/null +++ b/lib/berrypod_web/components/block_thumbnails.ex @@ -0,0 +1,478 @@ +defmodule BerrypodWeb.BlockThumbnails do + @moduledoc """ + Tiny SVG wireframe thumbnails for each block type. + + Used in the block picker to give a visual hint of what each block looks like. + Each thumbnail is an 80x48 SVG with simple geometric shapes. + """ + + use Phoenix.Component + + attr :type, :string, required: true + + def block_thumbnail(assigns) do + ~H""" + + """ + end + + # ── Hero banner ────────────────────────────────────────────────── + defp thumb_shapes(%{type: "hero"} = assigns) do + ~H""" + + + + + """ + end + + # ── Featured products ──────────────────────────────────────────── + defp thumb_shapes(%{type: "featured_products"} = assigns) do + ~H""" + + + + + + + + + + + + + """ + end + + # ── Image + text ───────────────────────────────────────────────── + defp thumb_shapes(%{type: "image_text"} = assigns) do + ~H""" + + + + + + + """ + end + + # ── Category navigation ────────────────────────────────────────── + defp thumb_shapes(%{type: "category_nav"} = assigns) do + ~H""" + + + + + """ + end + + # ── Newsletter signup ──────────────────────────────────────────── + defp thumb_shapes(%{type: "newsletter_card"} = assigns) do + ~H""" + + + + + + """ + end + + # ── Social links ───────────────────────────────────────────────── + defp thumb_shapes(%{type: "social_links_card"} = assigns) do + ~H""" + + + + + + + """ + end + + # ── Info card ──────────────────────────────────────────────────── + defp thumb_shapes(%{type: "info_card"} = assigns) do + ~H""" + + + + + + + + + """ + end + + # ── Trust badges ───────────────────────────────────────────────── + defp thumb_shapes(%{type: "trust_badges"} = assigns) do + ~H""" + + + + + + + """ + end + + # ── Customer reviews ───────────────────────────────────────────── + defp thumb_shapes(%{type: "reviews_section"} = assigns) do + ~H""" + + + + + + + + + + """ + end + + # ── Spacer ─────────────────────────────────────────────────────── + defp thumb_shapes(%{type: "spacer"} = assigns) do + ~H""" + + + + """ + end + + # ── Divider ────────────────────────────────────────────────────── + defp thumb_shapes(%{type: "divider"} = assigns) do + ~H""" + + """ + end + + # ── Button ─────────────────────────────────────────────────────── + defp thumb_shapes(%{type: "button"} = assigns) do + ~H""" + + + """ + end + + # ── Video embed ────────────────────────────────────────────────── + defp thumb_shapes(%{type: "video_embed"} = assigns) do + ~H""" + + + """ + end + + # ── Product hero ───────────────────────────────────────────────── + defp thumb_shapes(%{type: "product_hero"} = assigns) do + ~H""" + + + + + """ + end + + # ── Breadcrumb ─────────────────────────────────────────────────── + defp thumb_shapes(%{type: "breadcrumb"} = assigns) do + ~H""" + + + + + + """ + end + + # ── Product grid ───────────────────────────────────────────────── + defp thumb_shapes(%{type: t} = assigns) when t in ~w(product_grid related_products) do + ~H""" + + + + + + + """ + end + + # ── Collection header ──────────────────────────────────────────── + defp thumb_shapes(%{type: "collection_header"} = assigns) do + ~H""" + + + """ + end + + # ── Filter bar ─────────────────────────────────────────────────── + defp thumb_shapes(%{type: "filter_bar"} = assigns) do + ~H""" + + + + + """ + end + + # ── Cart items ─────────────────────────────────────────────────── + defp thumb_shapes(%{type: "cart_items"} = assigns) do + ~H""" + + + + + + + + """ + end + + # ── Order summary ──────────────────────────────────────────────── + defp thumb_shapes(%{type: "order_summary"} = assigns) do + ~H""" + + + + + + + + + + """ + end + + # ── Contact form ───────────────────────────────────────────────── + defp thumb_shapes(%{type: "contact_form"} = assigns) do + ~H""" + + + + + """ + end + + # ── Content body ───────────────────────────────────────────────── + defp thumb_shapes(%{type: "content_body"} = assigns) do + ~H""" + + + + + + + """ + end + + # ── Fallback for any unmatched type ────────────────────────────── + defp thumb_shapes(assigns) do + ~H""" + + + + """ + end +end diff --git a/lib/berrypod_web/components/shop_components/content.ex b/lib/berrypod_web/components/shop_components/content.ex index 24190f5..68a22c8 100644 --- a/lib/berrypod_web/components/shop_components/content.ex +++ b/lib/berrypod_web/components/shop_components/content.ex @@ -314,7 +314,7 @@ defmodule BerrypodWeb.ShopComponents.Content do

{@description}

- + <.shop_input type="email" placeholder="your@email.com" class="email-input" /> <.shop_button type="submit">{@button_text} @@ -329,7 +329,7 @@ defmodule BerrypodWeb.ShopComponents.Content do

{@description}

-
+ <.shop_input type="email" placeholder="your@email.com" class="email-input" /> <.shop_button type="submit">{@button_text}
diff --git a/lib/berrypod_web/page_renderer.ex b/lib/berrypod_web/page_renderer.ex index d9c2dd0..052bd2b 100644 --- a/lib/berrypod_web/page_renderer.ex +++ b/lib/berrypod_web/page_renderer.ex @@ -303,7 +303,15 @@ defmodule BerrypodWeb.PageRenderer do end defp render_block(%{block: %{"type" => "newsletter_card"}} = assigns) do - ~H"<.newsletter_card />" + settings = assigns.block["settings"] || %{} + + assigns = + assigns + |> assign(:title, settings["title"] || "Newsletter") + |> assign(:description, settings["description"] || "") + |> assign(:button_text, settings["button_text"] || "Subscribe") + + ~H"<.newsletter_card title={@title} description={@description} button_text={@button_text} />" end defp render_block(%{block: %{"type" => "social_links_card"}} = assigns) do