berrypod/lib/berrypod_web/live/admin/newsletter/campaign_form.ex

237 lines
7.4 KiB
Elixir
Raw Normal View History

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))}
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))}
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)
|> put_flash(:info, "Campaign saved")}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Please fill in subject and body")}
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("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 %>
</:subtitle>
</.header>
<div class="mt-6 max-w-2xl">
<.form for={@form} phx-change="validate" phx-submit="save_draft">
<div class="space-y-4">
<.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}}"
/>
<p :if={!readonly?(@campaign)} class="text-sm text-base-content/60">
Use <code>{"{{unsubscribe_url}}"}</code>
to insert the unsubscribe link. This is required for GDPR compliance.
</p>
<p
:if={missing_unsubscribe_url?(@form[:body].value) && !readonly?(@campaign)}
class="flex items-center gap-2 text-sm text-amber-700"
>
<.icon name="hero-exclamation-triangle" class="size-4 shrink-0" /> Body is missing
<code>{"{{unsubscribe_url}}"}</code>
this is required for GDPR compliance.
</p>
<%= if @form[:body].value && @form[:body].value != "" do %>
<details class="mt-4">
<summary class="text-sm font-medium cursor-pointer">Preview</summary>
<pre class="mt-2 p-4 bg-base-200 rounded-lg text-sm whitespace-pre-wrap overflow-auto max-h-64">{preview_body(@form[:body].value)}</pre>
</details>
<% end %>
<div
:if={!readonly?(@campaign)}
class="flex items-center gap-3 pt-4 border-t border-base-200"
>
<.button type="submit">
Save draft
</.button>
<button
type="button"
phx-click="send_now"
data-confirm={"Send this campaign to #{@subscriber_count} subscribers now?"}
class="admin-btn admin-btn-primary"
style="background-color: var(--color-green-600)"
disabled={@subscriber_count == 0}
>
<.icon name="hero-paper-airplane" class="size-4" /> Send now
</button>
<.link
navigate={~p"/admin/newsletter?tab=campaigns"}
class="admin-btn admin-btn-ghost"
>
Cancel
</.link>
</div>
<div :if={readonly?(@campaign)} class="pt-4 border-t border-base-200">
<.link navigate={~p"/admin/newsletter?tab=campaigns"} class="admin-btn admin-btn-ghost">
<.icon name="hero-arrow-left" class="size-4" /> Back to campaigns
</.link>
</div>
</div>
</.form>
</div>
"""
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