Packages

phoenix_kit

1.7.26
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
phoenix_kit lib modules emails web template_editor.html.heex
Raw

lib/modules/emails/web/template_editor.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
current_path={@url_path}
project_title={@project_title}
>
<div class="container flex-col mx-auto px-4 py-6">
<%!-- Header Section --%>
<header class="w-full relative mb-6">
<%!-- Back Button (Left aligned) --%>
<.link
navigate={Routes.path("/admin/modules/emails/templates")}
class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12"
>
<.icon_arrow_left /> Back to Templates
</.link>
<%!-- Title Section --%>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">{@page_title}</h1>
<%= if @mode == :edit and @template do %>
<% source_module = PhoenixKit.Modules.Emails.Template.get_source_module(@template) %>
<p class="text-lg text-base-content">
Version {@template.version} • {String.capitalize(@template.category)} • {String.capitalize(
source_module
)} • {String.capitalize(@template.status)}
</p>
<% else %>
<p class="text-lg text-base-content">Create a new email template</p>
<% end %>
</div>
</header>
<%!-- Main Editor Grid --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<%!-- Editor Panel --%>
<div class="space-y-6">
<.form
for={@changeset}
as={:email_template}
phx-change="validate"
phx-submit="save"
class="space-y-6"
>
<%!-- Basic Information --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Basic Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Template Name</span>
</label>
<input
type="text"
name="email_template[name]"
value={Ecto.Changeset.get_field(@changeset, :name)}
placeholder="welcome_email"
class="input input-bordered"
disabled={(@mode == :edit and @template) && @template.is_system}
/>
<%= if error = Keyword.get(@changeset.errors, :name) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Display Name</span>
</label>
<input
type="text"
name="email_template[display_name]"
value={Ecto.Changeset.get_field(@changeset, :display_name)}
placeholder="Welcome Email"
class="input input-bordered"
/>
<%= if error = Keyword.get(@changeset.errors, :display_name) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Slug</span>
</label>
<input
type="text"
name="email_template[slug]"
value={Ecto.Changeset.get_field(@changeset, :slug)}
placeholder="welcome-email"
class="input input-bordered"
disabled={(@mode == :edit and @template) && @template.is_system}
/>
<%= if error = Keyword.get(@changeset.errors, :slug) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
<label class="label">
<span class="label-text-alt">
URL-friendly identifier (auto-generated from name if left empty)
</span>
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Category</span>
</label>
<select
name="email_template[category]"
class="select select-bordered"
disabled={(@mode == :edit and @template) && @template.is_system}
>
<option
value="transactional"
selected={
Ecto.Changeset.get_field(@changeset, :category) == "transactional"
}
>
Transactional
</option>
<option
value="marketing"
selected={Ecto.Changeset.get_field(@changeset, :category) == "marketing"}
>
Marketing
</option>
<option
value="system"
selected={Ecto.Changeset.get_field(@changeset, :category) == "system"}
>
System
</option>
<option
value="notification"
selected={Ecto.Changeset.get_field(@changeset, :category) == "notification"}
>
Notification
</option>
</select>
<%= if error = Keyword.get(@changeset.errors, :category) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Source Module</span>
</label>
<% current_metadata = Ecto.Changeset.get_field(@changeset, :metadata) || %{} %>
<% current_source_module = Map.get(current_metadata, "source_module", "custom") %>
<select
name="email_template[metadata][source_module]"
class="select select-bordered"
>
<option value="users" selected={current_source_module == "users"}>
Users
</option>
<option value="billing" selected={current_source_module == "billing"}>
Billing
</option>
<option value="publishing" selected={current_source_module == "publishing"}>
Publishing
</option>
<option value="entities" selected={current_source_module == "entities"}>
Entities
</option>
<option value="admin" selected={current_source_module == "admin"}>
Admin
</option>
<option value="custom" selected={current_source_module == "custom"}>
Custom
</option>
</select>
<label class="label">
<span class="label-text-alt">Module that sends this email</span>
</label>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Status</span>
</label>
<select
name="email_template[status]"
class="select select-bordered"
>
<option
value="draft"
selected={Ecto.Changeset.get_field(@changeset, :status) == "draft"}
>
Draft
</option>
<option
value="active"
selected={Ecto.Changeset.get_field(@changeset, :status) == "active"}
>
Active
</option>
<option
value="archived"
selected={Ecto.Changeset.get_field(@changeset, :status) == "archived"}
>
Archived
</option>
</select>
<%= if error = Keyword.get(@changeset.errors, :status) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Description (Optional)</span>
</label>
<textarea
name="email_template[description]"
placeholder="Brief description of this template..."
class="textarea textarea-bordered"
rows="2"
><%= Ecto.Changeset.get_field(@changeset, :description) %></textarea>
<%= if error = Keyword.get(@changeset.errors, :description) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
</div>
</div>
</div>
<%!-- Email Content --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Email Content</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Subject Line</span>
</label>
<input
type="text"
name="email_template[subject]"
value={Ecto.Changeset.get_field(@changeset, :subject)}
placeholder="Welcome to {{app_name}}!"
class="input input-bordered"
/>
<%= if error = Keyword.get(@changeset.errors, :subject) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
<label class="label">
<span class="label-text-alt">
Use {"{{variable}}"} syntax for dynamic content
</span>
</label>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">HTML Body</span>
</label>
<textarea
name="email_template[html_body]"
class="textarea textarea-bordered font-mono"
rows="12"
placeholder="<h1>Welcome {{user_name}}!</h1>"
><%= Ecto.Changeset.get_field(@changeset, :html_body) %></textarea>
<%= if error = Keyword.get(@changeset.errors, :html_body) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Text Body</span>
</label>
<textarea
name="email_template[text_body]"
class="textarea textarea-bordered font-mono"
rows="8"
placeholder="Welcome {{user_name}}!"
><%= Ecto.Changeset.get_field(@changeset, :text_body) %></textarea>
<%= if error = Keyword.get(@changeset.errors, :text_body) do %>
<div class="text-sm text-error mt-1">{elem(error, 0)}</div>
<% end %>
</div>
</div>
</div>
<%!-- Template Variables --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Template Variables</h2>
<%= if length(@extracted_variables) > 0 do %>
<div class="alert alert-info mb-4">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div>
<div class="font-semibold">
{length(@extracted_variables)} variable(s) detected and auto-added
</div>
<div class="text-sm mt-1">
Variables are automatically managed. Edit descriptions below or remove if not needed.
</div>
</div>
</div>
<div class="space-y-2">
<%= for {name, description} <- Ecto.Changeset.get_field(@changeset, :variables) || %{} do %>
<div class="flex items-start gap-2 p-3 bg-base-200 rounded">
<div class="flex-shrink-0 pt-2">
<span class="font-mono text-sm font-semibold">{"{{#{name}}}"}</span>
</div>
<div class="flex-1">
<input
type="text"
value={description}
placeholder="Description for this variable..."
class="input input-sm input-bordered w-full"
phx-change="update_variable_description"
phx-value-name={name}
phx-debounce="300"
/>
<input
type="hidden"
name={"email_template[variables][#{name}]"}
value={description}
/>
</div>
<button
type="button"
phx-click="remove_variable"
phx-value-name={name}
class="btn btn-xs btn-ghost text-error mt-1"
title="Remove variable"
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-8 text-base-content/60">
<.icon name="hero-code-bracket" class="w-12 h-12 mx-auto mb-3 opacity-30" />
<div class="font-medium mb-2">No variables found in template</div>
<div class="text-sm">
Use {"{{variable_name}}"} syntax in subject or body to add dynamic variables.
</div>
<div class="text-sm mt-1">
Example: {"{{user_name}}"}, {"{{email}}"}, {"{{url}}"}
</div>
</div>
<% end %>
</div>
</div>
<%!-- Action Buttons --%>
<div class="flex justify-between">
<div class="flex gap-2">
<%!-- Test Send button (available in both new and edit modes) --%>
<button
type="button"
phx-click="show_test_modal"
class="btn btn-outline btn-secondary"
>
<.icon name="hero-envelope" class="w-4 h-4 mr-1" /> Test Send
</button>
<%!-- Save as Draft button (only in new mode) --%>
<%= if @mode == :new do %>
<button
type="submit"
name="save_as"
value="draft"
class="btn btn-outline btn-warning"
>
<.icon name="hero-document" class="w-4 h-4 mr-1" /> Save as Draft
</button>
<% end %>
</div>
<div class="flex gap-2">
<.link
navigate={Routes.path("/admin/modules/emails/templates")}
class="btn btn-ghost"
>
Cancel
</.link>
<button
type="submit"
class={[
"btn btn-primary",
@saving && "loading"
]}
disabled={@saving}
>
<%= if @saving do %>
Saving...
<% else %>
{if @mode == :new, do: "Create Template", else: "Update Template"}
<% end %>
</button>
</div>
</div>
</.form>
</div>
<%!-- Preview Panel --%>
<div class="space-y-6">
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title text-lg">Live Preview</h2>
<div class="btn-group">
<button
type="button"
phx-click="switch_preview"
phx-value-mode="html"
class={[
"btn btn-sm",
@preview_mode == "html" && "btn-active"
]}
>
HTML
</button>
<button
type="button"
phx-click="switch_preview"
phx-value-mode="text"
class={[
"btn btn-sm",
@preview_mode == "text" && "btn-active"
]}
>
Text
</button>
</div>
</div>
<%!-- Subject Preview --%>
<div class="mb-4">
<div class="text-sm font-medium text-base-content/70 mb-1">Subject:</div>
<div class="p-2 bg-base-200 rounded text-sm">
{Ecto.Changeset.get_field(@changeset, :subject) || "No subject"}
</div>
</div>
<%!-- Body Preview --%>
<div class="border rounded-lg overflow-hidden">
<%= if @preview_mode == "html" do %>
<%= case Ecto.Changeset.get_field(@changeset, :html_body) do %>
<% html_content when is_binary(html_content) and html_content != "" -> %>
<iframe
id="email-preview-iframe"
sandbox="allow-same-origin"
class="w-full border-0 bg-white"
style="min-height: 400px; height: auto;"
srcdoc={html_content}
phx-update="ignore"
onload="this.style.height = (this.contentWindow.document.body.scrollHeight + 40) + 'px';"
>
</iframe>
<% _ -> %>
<div class="bg-white p-4 min-h-96">
<div class="text-gray-500 italic">No HTML content</div>
</div>
<% end %>
<% else %>
<div class="bg-gray-50 p-4 min-h-96 font-mono text-sm whitespace-pre-wrap">
<%= case Ecto.Changeset.get_field(@changeset, :text_body) do %>
<% nil -> %>
<div class="text-gray-500 italic not-italic">No text content</div>
<% "" -> %>
<div class="text-gray-500 italic not-italic">No text content</div>
<% text_content -> %>
{text_content}
<% end %>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
<%!-- Test Email Modal --%>
<div
:if={@show_test_modal}
class="modal modal-open"
phx-click-away="hide_test_modal"
>
<div class="modal-box max-w-4xl">
<h3 class="font-bold text-lg mb-4">Send Test Email</h3>
<.form
for={%{}}
phx-submit="send_test"
phx-change="validate_test"
class="space-y-4"
>
<div class="form-control">
<label class="label">
<span class="label-text">Recipient Email</span>
</label>
<input
type="email"
name="test[recipient]"
value={@test_form[:recipient] || ""}
placeholder="admin@example.com"
class={[
"input input-bordered w-full",
@test_form[:errors][:recipient] && "input-error"
]}
required
/>
<%= if @test_form[:errors][:recipient] do %>
<label class="label">
<span class="label-text-alt text-error">
{@test_form[:errors][:recipient]}
</span>
</label>
<% end %>
</div>
<%= if length(@extracted_variables) > 0 do %>
<div class="form-control">
<label class="label">
<span class="label-text">Sample Variable Values</span>
</label>
<div class="space-y-2">
<%= for variable <- @extracted_variables do %>
<div class="grid grid-cols-3 gap-2 items-center">
<label class="text-sm font-mono">{"{{#{variable}}}"}</label>
<div class="col-span-2">
<input
type="text"
name={"test[sample_variables][#{variable}]"}
value={@test_form[:sample_variables][variable] || ""}
placeholder={"Sample #{variable}"}
class="input input-bordered input-sm w-full"
/>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<div class="alert alert-info">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div class="text-sm">
This will send a test email using the current template content with the sample variables provided above.
</div>
</div>
<div class="modal-action">
<button
type="button"
phx-click="hide_test_modal"
class="btn btn-ghost"
>
Cancel
</button>
<button
type="submit"
class={[
"btn btn-primary",
@test_sending && "loading"
]}
disabled={@test_sending}
>
<%= if @test_sending do %>
Sending...
<% else %>
Send Test Email
<% end %>
</button>
</div>
</.form>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>