Packages
phoenix_kit
1.7.69
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/newsletters/web/broadcast_editor.ex
defmodule PhoenixKit.Modules.Newsletters.Web.BroadcastEditor do
@moduledoc """
LiveView for creating and editing newsletter broadcasts with Markdown editor and live preview.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Modules.Emails.Template
alias PhoenixKit.Modules.Emails.Templates
alias PhoenixKit.Modules.Newsletters
alias PhoenixKit.Modules.Newsletters.Broadcaster
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
@impl true
def mount(_params, _session, socket) do
if Newsletters.enabled?() do
project_title = Settings.get_project_title()
lists = Newsletters.list_lists(%{status: "active"})
templates = Templates.list_templates(%{status: "active"})
default_template_uuid = Settings.get_setting("newsletters_default_template")
socket =
socket
|> assign(:page_title, "New Broadcast")
|> assign(:project_title, project_title)
|> assign(:url_path, Routes.path("/admin/newsletters/broadcasts/new"))
|> assign(:lists, lists)
|> assign(:templates, templates)
|> assign(:broadcast, nil)
|> assign(:subject, "")
|> assign(:list_uuid, "")
|> assign(:template_uuid, default_template_uuid || "")
|> assign(:markdown_content, "")
|> assign(:preview_html, "")
|> assign(:scheduled_at, "")
|> assign(:saving, false)
{:ok, socket}
else
{:ok,
socket
|> put_flash(:error, "Newsletters module is not enabled")
|> push_navigate(to: Routes.path("/admin"))}
end
end
@impl true
def handle_params(%{"id" => id}, _url, %{assigns: %{live_action: :edit}} = socket) do
broadcast = Newsletters.get_broadcast!(id)
{:noreply,
socket
|> assign(:page_title, "Edit Broadcast")
|> assign(:url_path, Routes.path("/admin/newsletters/broadcasts/#{id}/edit"))
|> assign(:broadcast, broadcast)
|> assign(:subject, broadcast.subject || "")
|> assign(:list_uuid, broadcast.list_uuid || "")
|> assign(:template_uuid, broadcast.template_uuid || "")
|> assign(:markdown_content, broadcast.markdown_body || "")
|> assign(
:preview_html,
render_preview(broadcast.markdown_body, broadcast.template_uuid, socket.assigns.templates)
)}
rescue
Ecto.NoResultsError ->
{:noreply,
socket
|> put_flash(:error, "Broadcast not found")
|> push_navigate(to: Routes.path("/admin/newsletters/broadcasts"))}
end
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl true
def handle_event("validate", params, socket) do
subject = params["subject"] || socket.assigns.subject
list_uuid = params["list_uuid"] || socket.assigns.list_uuid
template_uuid = params["template_uuid"] || socket.assigns.template_uuid
scheduled_at = params["scheduled_at"] || socket.assigns.scheduled_at
preview_html =
render_preview(socket.assigns.markdown_content, template_uuid, socket.assigns.templates)
{:noreply,
socket
|> assign(:subject, subject)
|> assign(:list_uuid, list_uuid)
|> assign(:template_uuid, template_uuid)
|> assign(:scheduled_at, scheduled_at)
|> assign(:preview_html, preview_html)}
end
@impl true
def handle_event("save_draft", params, socket) do
socket = update_assigns_from_params(socket, params)
save_broadcast(socket, "draft")
end
@impl true
def handle_event("send_now", params, socket) do
socket = update_assigns_from_params(socket, params)
case save_broadcast_and_return(socket) do
{:ok, broadcast} ->
case Broadcaster.send(broadcast) do
{:ok, _broadcast} ->
{:noreply,
socket
|> put_flash(:info, "Broadcast is being sent")
|> push_navigate(to: Routes.path("/admin/newsletters/broadcasts/#{broadcast.uuid}"))}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Failed to send: #{inspect(reason)}")}
end
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Failed to save: #{inspect(reason)}")}
end
end
@impl true
def handle_event("schedule", params, socket) do
socket = update_assigns_from_params(socket, params)
case socket.assigns.scheduled_at do
"" ->
{:noreply, put_flash(socket, :error, "Please select a schedule date and time")}
scheduled_at_str ->
save_broadcast(socket, "scheduled", %{scheduled_at: parse_datetime(scheduled_at_str)})
end
end
@impl true
def handle_info({:editor_content_changed, %{content: content}}, socket) do
preview_html = render_preview(content, socket.assigns.template_uuid, socket.assigns.templates)
{:noreply,
socket
|> assign(:markdown_content, content)
|> assign(:preview_html, preview_html)}
end
@impl true
def handle_info(_msg, socket), do: {:noreply, socket}
# --- Private ---
defp update_assigns_from_params(socket, params) do
socket
|> assign(:subject, params["subject"] || socket.assigns.subject)
|> assign(:list_uuid, params["list_uuid"] || socket.assigns.list_uuid)
|> assign(:template_uuid, params["template_uuid"] || socket.assigns.template_uuid)
|> assign(:scheduled_at, params["scheduled_at"] || socket.assigns.scheduled_at)
end
defp save_broadcast(socket, status, extra_attrs \\ %{}) do
socket = assign(socket, :saving, true)
attrs =
Map.merge(
%{
subject: socket.assigns.subject,
list_uuid: socket.assigns.list_uuid,
template_uuid:
if(socket.assigns.template_uuid == "", do: nil, else: socket.assigns.template_uuid),
markdown_body: socket.assigns.markdown_content,
status: status
},
extra_attrs
)
result =
case socket.assigns.broadcast do
nil -> Newsletters.create_broadcast(attrs)
broadcast -> Newsletters.update_broadcast(broadcast, attrs)
end
case result do
{:ok, broadcast} ->
{:noreply,
socket
|> assign(:saving, false)
|> assign(:broadcast, broadcast)
|> put_flash(:info, "Broadcast saved as #{status}")
|> push_navigate(to: Routes.path("/admin/newsletters/broadcasts"))}
{:error, changeset} ->
errors =
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|> Enum.map_join(", ", fn {field, msgs} -> "#{field}: #{Enum.join(msgs, ", ")}" end)
{:noreply,
socket
|> assign(:saving, false)
|> put_flash(:error, "Validation failed: #{errors}")}
end
end
defp save_broadcast_and_return(socket) do
attrs = %{
subject: socket.assigns.subject,
list_uuid: socket.assigns.list_uuid,
template_uuid:
if(socket.assigns.template_uuid == "", do: nil, else: socket.assigns.template_uuid),
markdown_body: socket.assigns.markdown_content,
status: "draft"
}
case socket.assigns.broadcast do
nil -> Newsletters.create_broadcast(attrs)
broadcast -> Newsletters.update_broadcast(broadcast, attrs)
end
end
defp render_preview(markdown, template_uuid, templates) do
case Earmark.as_html(markdown || "") do
{:ok, html, _} ->
inject_into_template(html, template_uuid, templates)
_ ->
""
end
end
defp inject_into_template(html, template_uuid, templates)
when is_binary(template_uuid) and template_uuid != "" do
case Enum.find(templates, fn t -> t.uuid == template_uuid end) do
nil ->
html
template ->
html_template = Template.get_translation(template.html_body, "en")
String.replace(html_template, "{{content}}", html)
end
end
defp inject_into_template(html, _, _), do: html
defp parse_datetime(str) when is_binary(str) do
case DateTime.from_iso8601(str <> ":00Z") do
{:ok, dt, _} ->
dt
_ ->
case NaiveDateTime.from_iso8601(str <> ":00") do
{:ok, ndt} -> DateTime.from_naive!(ndt, "Etc/UTC")
_ -> nil
end
end
end
defp parse_datetime(_), do: nil
end