defmodule BerrypodWeb.Admin.Newsletter.CampaignForm do use BerrypodWeb, :live_view alias Berrypod.Newsletter @impl true def mount(_params, _session, socket) do {:ok, socket |> assign(:page_title, "New campaign") |> assign(:campaign, nil) |> assign(:subscriber_count, Newsletter.confirmed_subscriber_count()) |> assign(:form, to_form(%{"subject" => "", "body" => ""}, as: :campaign)) |> assign(:save_status, :idle)} end @impl true def handle_params(%{"id" => id}, _uri, socket) do campaign = Newsletter.get_campaign!(id) title = if campaign.status == "draft", do: "Edit campaign", else: "Campaign: #{campaign.subject}" {:noreply, socket |> assign(:page_title, title) |> assign(:campaign, campaign) |> assign( :form, to_form( %{ "subject" => campaign.subject, "body" => campaign.body }, as: :campaign ) )} end def handle_params(_params, _uri, socket), do: {:noreply, socket} @impl true def handle_event("validate", %{"campaign" => params}, socket) do {:noreply, assign(socket, form: to_form(params, as: :campaign), save_status: :idle)} end def handle_event("save_draft", %{"campaign" => params}, socket) do case save_campaign(socket.assigns.campaign, params) do {:ok, campaign} -> {:noreply, socket |> assign(:campaign, campaign) |> assign(:save_status, :saved)} {:error, _changeset} -> {:noreply, assign(socket, :save_status, :error)} end end def handle_event("send_now", _params, socket) do params = current_form_params(socket) with {:ok, campaign} <- save_campaign(socket.assigns.campaign, params), {:ok, campaign} <- Newsletter.send_campaign_now(campaign) do {:noreply, socket |> assign(:campaign, campaign) |> put_flash(:info, "Campaign is being sent!") |> push_navigate(to: ~p"/admin/newsletter?tab=campaigns")} else {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to send campaign")} end end def handle_event("send_test", _params, socket) do params = current_form_params(socket) to_email = socket.assigns.current_scope.user.email if params["subject"] == "" || params["body"] == "" do {:noreply, put_flash(socket, :error, "Fill in subject and body first")} else # Build a temporary campaign struct for the notifier campaign = %Newsletter.Campaign{ subject: params["subject"], body: params["body"] } case Newsletter.Notifier.deliver_test(campaign, to_email) do {:ok, _} -> {:noreply, put_flash(socket, :info, "Test email sent to #{to_email}")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to send test email")} end end end def handle_event("schedule", _params, socket) do params = current_form_params(socket) scheduled_at = parse_schedule_time(params["scheduled_at"]) with {:ok, campaign} <- save_campaign(socket.assigns.campaign, params), {:ok, _campaign} <- Newsletter.schedule_campaign(campaign, scheduled_at) do {:noreply, socket |> put_flash(:info, "Campaign scheduled") |> push_navigate(to: ~p"/admin/newsletter?tab=campaigns")} else {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to schedule campaign")} end end defp save_campaign(nil, params) do Newsletter.create_campaign(%{ subject: params["subject"], body: params["body"] }) end defp save_campaign(%{status: "draft"} = campaign, params) do Newsletter.update_campaign(campaign, %{ subject: params["subject"], body: params["body"] }) end defp save_campaign(campaign, _params), do: {:ok, campaign} defp parse_schedule_time(nil), do: DateTime.utc_now() |> DateTime.add(3600) defp parse_schedule_time(""), do: DateTime.utc_now() |> DateTime.add(3600) defp parse_schedule_time(str) do case DateTime.from_iso8601(str <> ":00Z") do {:ok, dt, _} -> dt _ -> DateTime.utc_now() |> DateTime.add(3600) end end @impl true def render(assigns) do ~H""" <.header> {@page_title} <:subtitle> <%= cond do %> <% readonly?(@campaign) -> %> This campaign was sent on {format_date(@campaign.sent_at)} to {@campaign.sent_count} subscribers <% @subscriber_count > 0 -> %> {@subscriber_count} confirmed subscribers will receive this email <% true -> %> No confirmed subscribers yet <% end %>
<.form for={@form} phx-change="validate" phx-submit="save_draft">
<.input field={@form[:subject]} type="text" label="Subject" required disabled={readonly?(@campaign)} placeholder="Your campaign subject" /> <.input field={@form[:body]} type="textarea" label="Body (plain text)" required disabled={readonly?(@campaign)} rows="12" placeholder="Hello!\n\nYour newsletter content here.\n\nUnsubscribe: {{unsubscribe_url}}" />

Use {"{{unsubscribe_url}}"} to insert the unsubscribe link. This is required for GDPR compliance.

<.icon name="hero-exclamation-triangle" class="size-4 shrink-0" /> Body is missing {"{{unsubscribe_url}}"} — this is required for GDPR compliance.

<%= if @form[:body].value && @form[:body].value != "" do %>
Preview
{preview_body(@form[:body].value)}
<% end %>
<.button type="submit"> Save draft <.inline_feedback status={@save_status} /> <.link navigate={~p"/admin/newsletter?tab=campaigns"} class="admin-btn admin-btn-ghost" > Cancel
<.link navigate={~p"/admin/newsletter?tab=campaigns"} class="admin-btn admin-btn-ghost"> <.icon name="hero-arrow-left" class="size-4" /> Back to campaigns
""" end defp current_form_params(socket) do form = socket.assigns.form %{"subject" => form[:subject].value || "", "body" => form[:body].value || ""} end defp readonly?(%{status: status}) when status not in ["draft"], do: true defp readonly?(_), do: false defp missing_unsubscribe_url?(nil), do: false defp missing_unsubscribe_url?(""), do: false defp missing_unsubscribe_url?(body), do: not String.contains?(body, "{{unsubscribe_url}}") defp format_date(nil), do: "—" defp format_date(datetime), do: Calendar.strftime(datetime, "%-d %b %Y") defp preview_body(body) do sample_url = BerrypodWeb.Endpoint.url() <> "/unsubscribe/sample-token" String.replace(body, "{{unsubscribe_url}}", sample_url) end end