defmodule PhoenixKit.Modules.Blogging.Web.New do @moduledoc """ LiveView for creating a new blog workspace. """ use PhoenixKitWeb, :live_view use Gettext, backend: PhoenixKitWeb.Gettext alias PhoenixKit.Modules.Blogging alias PhoenixKit.Modules.Blogging.PubSub, as: BloggingPubSub alias PhoenixKit.Modules.Blogging.Storage alias PhoenixKit.Settings alias PhoenixKit.Utils.Routes def mount(_params, _session, socket) do {initial_params, auto?, last_generated_slug} = normalize_slug_params(%{}, true, "") socket = socket |> assign(:project_title, Settings.get_setting("project_title", "PhoenixKit")) |> assign(:page_title, gettext("Create Blog")) |> assign( :current_path, Routes.path("/admin/settings/blogging/new", locale: socket.assigns.current_locale_base) ) |> assign(:slug_autogenerated?, auto?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:form_params, initial_params) |> assign(:form, new_blog_form(initial_params)) |> assign(:enabled_languages, Storage.enabled_language_codes()) |> assign(:endpoint_url, nil) {:ok, socket} end def handle_params(_params, uri, socket) do endpoint_url = extract_endpoint_url(uri) {:noreply, assign(socket, :endpoint_url, endpoint_url)} end def handle_event("update_new_blog", %{"blog" => params}, socket) do {normalized_params, auto?, last_generated_slug} = normalize_slug_params( params, socket.assigns.slug_autogenerated?, socket.assigns.last_generated_slug ) {:noreply, socket |> assign(:slug_autogenerated?, auto?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:form_params, normalized_params) |> assign(:form, new_blog_form(normalized_params))} end def handle_event("manual_slug", %{"blog" => %{"slug" => value}}, socket) do current_params = socket.assigns.form_params name = Map.get(current_params, "name", "") auto_slug = if name == "", do: "", else: Blogging.slugify(name) trimmed_value = value |> to_string() |> String.trim() cond do # Empty slug - revert to auto-generated trimmed_value == "" -> updated = Map.put(current_params, "slug", auto_slug) {:noreply, socket |> assign(:slug_autogenerated?, true) |> assign(:last_generated_slug, auto_slug) |> assign(:form_params, updated) |> assign(:form, new_blog_form(updated))} # Manually entered slug matches auto-generated - keep autogenerated flag trimmed_value == auto_slug -> updated = Map.put(current_params, "slug", auto_slug) {:noreply, socket |> assign(:slug_autogenerated?, true) |> assign(:last_generated_slug, auto_slug) |> assign(:form_params, updated) |> assign(:form, new_blog_form(updated))} # Any other manual slug - just store it as-is (validation happens on submit) true -> updated = Map.put(current_params, "slug", trimmed_value) {:noreply, socket |> assign(:slug_autogenerated?, false) |> assign(:last_generated_slug, trimmed_value) |> assign(:form_params, updated) |> assign(:form, new_blog_form(updated)) |> clear_flash()} end end def handle_event("add_blog", %{"blog" => params}, socket) do # Use raw params for validation - don't normalize/sanitize name = params |> Map.get("name", "") |> String.trim() mode = params |> Map.get("mode", "timestamp") slug = params |> Map.get("slug", "") |> String.trim() # Still normalize for UI updates in case of error {normalized_params, auto?, last_generated_slug} = normalize_slug_params( params, socket.assigns.slug_autogenerated?, socket.assigns.last_generated_slug ) # Pass empty string if slug is empty to trigger auto-generation slug_to_validate = if slug == "", do: nil, else: slug case Blogging.add_blog(name, mode, slug_to_validate) do {:ok, blog} -> # Broadcast blog created for live dashboard updates BloggingPubSub.broadcast_blog_created(blog) {:noreply, socket |> put_flash(:info, gettext("Blog \"%{name}\" created", name: blog["name"])) |> push_navigate( to: Routes.path("/admin/settings/blogging", locale: socket.assigns.current_locale_base) )} {:error, :already_exists} -> {:noreply, socket |> assign(:slug_autogenerated?, auto?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:form_params, normalized_params) |> assign(:form, new_blog_form(normalized_params)) |> put_flash(:error, gettext("That blog already exists"))} {:error, :invalid_name} -> {:noreply, socket |> assign(:slug_autogenerated?, auto?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:form_params, normalized_params) |> assign(:form, new_blog_form(normalized_params)) |> put_flash(:error, gettext("Please enter a valid blog name"))} {:error, :invalid_mode} -> {:noreply, socket |> assign(:slug_autogenerated?, auto?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:form_params, normalized_params) |> assign(:form, new_blog_form(normalized_params)) |> put_flash(:error, gettext("Invalid storage mode"))} {:error, :invalid_slug} -> {:noreply, socket |> assign(:slug_autogenerated?, auto?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:form_params, normalized_params) |> assign(:form, new_blog_form(normalized_params)) |> put_flash( :error, gettext( "Invalid slug format. Please use only lowercase letters, numbers, and hyphens (e.g. my-blog-name)" ) )} {:error, _reason} -> {:noreply, socket |> assign(:slug_autogenerated?, auto?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:form_params, normalized_params) |> assign(:form, new_blog_form(normalized_params)) |> put_flash(:error, gettext("Failed to add blog"))} end end def handle_event("cancel", _params, socket) do {:noreply, push_navigate(socket, to: Routes.path("/admin/settings/blogging", locale: socket.assigns.current_locale_base) )} end defp new_blog_form(params) when is_map(params) do defaults = %{"name" => "", "slug" => "", "mode" => "timestamp"} values = Map.merge(defaults, params) to_form(values, as: :blog) end defp normalize_slug_params(params, slug_autogenerated?, last_generated_slug) do mode = params |> Map.get("mode", "timestamp") |> to_string() |> String.downcase() |> case do "slug" -> "slug" _ -> "timestamp" end name = params |> Map.get("name", "") |> to_string() |> String.trim() slug_input = params |> Map.get("slug", "") |> to_string() trimmed_slug = String.trim(slug_input) # Keep user input as-is for validation - don't sanitize here trimmed_input = trimmed_slug auto_slug = if name == "" do "" else Blogging.slugify(name) end if slug_autogenerated? do slug_value = auto_slug {%{"name" => name, "slug" => slug_value, "mode" => mode}, true, slug_value} else slug_value = case trimmed_input do "" -> auto_slug value -> value end auto? = slug_value == auto_slug new_last_generated = if auto?, do: slug_value, else: last_generated_slug {%{"name" => name, "slug" => slug_value, "mode" => mode}, auto?, new_last_generated} end end defp extract_endpoint_url(uri) when is_binary(uri) do case URI.parse(uri) do %URI{scheme: scheme, host: host, port: port} when not is_nil(scheme) and not is_nil(host) -> port_string = if port in [80, 443], do: "", else: ":#{port}" "#{scheme}://#{host}#{port_string}" _ -> "" end end defp extract_endpoint_url(_), do: "" end