From de343e726f59271f27311653c99ff8b2558f4fe3 Mon Sep 17 00:00:00 2001 From: Max Don Date: Mon, 17 Nov 2025 00:02:49 +0200 Subject: [PATCH 1/3] Update blog editor featured image selector UI for better UX Redesigned the featured image selector integration with improved visual hierarchy and responsive behavior: - Replace technical label "Featured image file ID" with user-friendly "Featured Image" - Add responsive image preview with hover-based actions on desktop - Implement mobile-friendly always-visible action buttons below preview - Create large clickable empty state area with visual affordances - Minimize advanced manual ID entry section with custom flexbox-based disclosure - Remove default browser disclosure marker to avoid duplicate arrows - Add smooth transitions and hover effects for better interactivity The new design provides a cleaner, more intuitive interface that works seamlessly on both desktop and mobile devices. --- .../live/modules/blogging/editor.html.heex | 139 +++++++++++------- 1 file changed, 86 insertions(+), 53 deletions(-) diff --git a/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex b/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex index 83e515387..b6388f526 100644 --- a/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex +++ b/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex @@ -192,77 +192,110 @@
- <%!-- Media Selector Button --%> -
+ <%= if preview_url = featured_image_preview_url(@form["featured_image_id"]) do %> + <%!-- Image Preview with Actions --%> +
+
+ {@post.metadata.title + <%!-- Desktop: Hover overlay --%> + +
+ <%!-- Mobile: Always visible buttons --%> +
+ + +
+
+ <% else %> + <%!-- No Image Selected - Show Upload Area --%> - <%= if @form["featured_image_id"] && @form["featured_image_id"] != "" do %> - - <% end %> -
+ <% end %> - <%!-- Manual Input (Advanced) --%> -
- - {gettext("Advanced: Enter File ID Manually")} + <%!-- Advanced: Manual ID Entry (Collapsed by default) --%> +
+ +
+ <.icon + name="hero-chevron-right" + class="w-3 h-3 transition-transform [[open]>&]:rotate-90" + /> + + {gettext("Advanced: Manual File ID")} + +
-
+
-

- {gettext("Enter a file ID directly if you know it.")} +

+ {gettext("Paste a PhoenixKit Storage file ID if you know it.")}

- <%= if preview_url = featured_image_preview_url(@form["featured_image_id"]) do %> -
- {@post.metadata.title -

- {gettext("Clear the field above to remove the featured image.")} -

-

- {gettext("Valid image ID")} -

-
- <% else %> - <% trimmed_id = @form["featured_image_id"] |> to_string() |> String.trim() %> - <%= if trimmed_id == "" do %> -

- {gettext("No featured image selected.")} -

- <% else %> -

- {gettext("Invalid image ID or file not found.")} -

- <% end %> - <% end %>
From 360b2b56b1b867aa46fa9fd353cdfe257b01f104 Mon Sep 17 00:00:00 2001 From: Max Don Date: Mon, 17 Nov 2025 00:44:07 +0200 Subject: [PATCH 2/3] Improve blogging editor component insertion and video rendering --- lib/phoenix_kit/blogging/renderer.ex | 71 ++++++- .../live/modules/blogging/editor.ex | 69 ++++++- .../live/modules/blogging/editor.html.heex | 178 +++++++++++++++++- 3 files changed, 304 insertions(+), 14 deletions(-) diff --git a/lib/phoenix_kit/blogging/renderer.ex b/lib/phoenix_kit/blogging/renderer.ex index 4283e054a..7e6be3286 100644 --- a/lib/phoenix_kit/blogging/renderer.ex +++ b/lib/phoenix_kit/blogging/renderer.ex @@ -16,6 +16,7 @@ defmodule PhoenixKit.Blogging.Renderer do @cache_name :blog_posts @cache_version "v1" @component_regex ~r/<(Image|Hero|CTA|Headline|Subheadline|Video)\s+([^>]*?)\/>/s + @component_block_regex ~r/<(Hero|CTA|Headline|Subheadline|Video)\s*([^>]*)>(.*?)<\/\1>/s @doc """ Renders a post's markdown content to HTML. @@ -86,8 +87,11 @@ defmodule PhoenixKit.Blogging.Renderer do # Detect if markdown content has embedded XML components defp has_embedded_components?(content) do String.contains?(content, " [render_earmark_markdown(content) | acc] - [{match_start, match_len}, {tag_start, tag_len}, {attrs_start, attrs_len}] -> + {:self_closing, [{match_start, match_len}, {tag_start, tag_len}, {attrs_start, attrs_len}]} -> before = binary_part(content, 0, match_start) after_index = match_start + match_len rest_content = binary_part(content, after_index, byte_size(content) - after_index) @@ -155,6 +159,46 @@ defmodule PhoenixKit.Blogging.Renderer do |> add_component(tag, attrs) render_mixed_segments(rest_content, acc) + + {:block, indexes} -> + [{match_start, match_len} | _rest] = indexes + before = binary_part(content, 0, match_start) + after_index = match_start + match_len + rest_content = binary_part(content, after_index, byte_size(content) - after_index) + fragment = binary_part(content, match_start, match_len) + + acc = + acc + |> maybe_add_markdown(before) + |> add_block_component(fragment) + + render_mixed_segments(rest_content, acc) + end + end + + defp next_component_match(content) do + self_match = Regex.run(@component_regex, content, return: :index) + block_match = Regex.run(@component_block_regex, content, return: :index) + + case {self_match, block_match} do + {nil, nil} -> + nil + + {nil, block} -> + {:block, block} + + {self, nil} -> + {:self_closing, self} + + {self, block} -> + self_start = self |> hd() |> elem(0) + block_start = block |> hd() |> elem(0) + + if self_start <= block_start do + {:self_closing, self} + else + {:block, block} + end end end @@ -168,6 +212,10 @@ defmodule PhoenixKit.Blogging.Renderer do [render_inline_component(tag, attrs) | acc] end + defp add_block_component(acc, fragment) do + [render_block_component(fragment) | acc] + end + # Render individual inline component defp render_inline_component("Image", attrs) do # Parse attributes @@ -216,6 +264,21 @@ defmodule PhoenixKit.Blogging.Renderer do "" end + defp render_block_component(fragment) do + fragment + |> PageBuilder.render_content() + |> case do + {:ok, html} -> + html + |> Safe.to_iodata() + |> IO.iodata_to_binary() + + {:error, reason} -> + Logger.warning("Error rendering block component: #{inspect(reason)}") + "
Error rendering component
" + end + end + # Parse XML attribute string into a map defp parse_xml_attributes(attrs_string) do # Match key="value" or key='value' patterns diff --git a/lib/phoenix_kit_web/live/modules/blogging/editor.ex b/lib/phoenix_kit_web/live/modules/blogging/editor.ex index 3c4515275..86d96355d 100644 --- a/lib/phoenix_kit_web/live/modules/blogging/editor.ex +++ b/lib/phoenix_kit_web/live/modules/blogging/editor.ex @@ -209,6 +209,42 @@ defmodule PhoenixKitWeb.Live.Modules.Blogging.Editor do {:noreply, assign(socket, :show_media_selector, true)} end + def handle_event("open_image_component_selector", _params, socket) do + {:noreply, + socket + |> assign(:show_media_selector, true) + |> assign(:inserting_image_component, true)} + end + + def handle_event("insert_component", %{"component" => "video"}, socket) do + {:noreply, + push_event(socket, "phx:prompt-and-insert", %{ + component: "video", + prompt: "Enter YouTube URL:", + placeholder: "https://youtu.be/dQw4w9WgXcQ" + })} + end + + def handle_event("insert_component", %{"component" => "cta"}, socket) do + template = """ + Button Text + """ + + {:noreply, push_event(socket, "phx:insert-at-cursor", %{text: template})} + end + + def handle_event("insert_video_component", %{"url" => url}, socket) do + template = """ + + + + """ + + {:noreply, push_event(socket, "phx:insert-at-cursor", %{text: template})} + end + def handle_event("clear_featured_image", _params, socket) do # Clear the featured image from the form (form is a simple map, not a struct) updated_form = Map.put(socket.assigns.form, "featured_image_id", "") @@ -435,25 +471,40 @@ defmodule PhoenixKitWeb.Live.Modules.Blogging.Editor do def handle_info({:media_selected, file_ids}, socket) do # Handle the selected file IDs from the media selector modal file_id = List.first(file_ids) + inserting_image_component = Map.get(socket.assigns, :inserting_image_component, false) socket = - if file_id do + if file_id && inserting_image_component do + # Insert image component at cursor via JavaScript + js_code = "window.insertImageComponent && window.insertImageComponent('#{file_id}')" + socket - |> assign(:form, update_form_with_media(socket.assigns.form, file_id)) - |> assign(:has_pending_changes, true) |> assign(:show_media_selector, false) - |> put_flash(:info, gettext("Featured image selected")) - |> push_event("changes-status", %{has_changes: true}) + |> assign(:inserting_image_component, false) + |> put_flash(:info, gettext("Image component inserted")) + |> push_event("exec-js", %{js: js_code}) else - socket - |> assign(:show_media_selector, false) + if file_id do + socket + |> assign(:form, update_form_with_media(socket.assigns.form, file_id)) + |> assign(:has_pending_changes, true) + |> assign(:show_media_selector, false) + |> put_flash(:info, gettext("Featured image selected")) + |> push_event("changes-status", %{has_changes: true}) + else + socket + |> assign(:show_media_selector, false) + end end {:noreply, socket} end def handle_info({:media_selector_closed}, socket) do - {:noreply, assign(socket, :show_media_selector, false)} + {:noreply, + socket + |> assign(:show_media_selector, false) + |> assign(:inserting_image_component, false)} end defp create_new_post(socket, params) do @@ -701,7 +752,7 @@ defmodule PhoenixKitWeb.Live.Modules.Blogging.Editor do DateTime.utc_now() |> floor_datetime_to_minute() |> DateTime.to_iso8601(), - "featured_image_id" => post.metadata.featured_image_id || "" + "featured_image_id" => Map.get(post.metadata, :featured_image_id, "") } form = diff --git a/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex b/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex index b6388f526..a8825ba80 100644 --- a/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex +++ b/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex @@ -10,6 +10,143 @@ >