301 lines
14 KiB
Plaintext
301 lines
14 KiB
Plaintext
@using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels
|
|
@model EmailTemplateEditViewModel
|
|
@{
|
|
Layout = "~/Views/Shared/_EditorLayout.cshtml";
|
|
ViewData["Title"] = Model.PageTitle;
|
|
ViewData["EditorTitle"] = Model.PageTitle;
|
|
}
|
|
|
|
<form id="templateForm" asp-action="Save" asp-controller="AdminTemplates" method="post">
|
|
@Html.AntiForgeryToken()
|
|
<input asp-for="Id" type="hidden" />
|
|
<input type="hidden" name="IsNew" value="@Model.IsNew" />
|
|
<input asp-for="ServiceNameFilter" type="hidden" />
|
|
<input asp-for="KeyFilter" type="hidden" />
|
|
|
|
@if (!ViewData.ModelState.IsValid)
|
|
{
|
|
<div class="alert alert-danger mb-3">
|
|
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
|
{
|
|
<div>@error.ErrorMessage</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<ul class="nav nav-tabs template-editor-tabs mb-3" id="templateEditorTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit-pane" type="button" role="tab" aria-controls="edit-pane" aria-selected="true">
|
|
Edit
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="preview-tab" data-bs-toggle="tab" data-bs-target="#preview-pane" type="button" role="tab" aria-controls="preview-pane" aria-selected="false">
|
|
Preview
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-pane fade show active" id="edit-pane" role="tabpanel" aria-labelledby="edit-tab" tabindex="0">
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-5">
|
|
<label asp-for="ServiceName" class="form-label fw-semibold">Service Name</label>
|
|
<input asp-for="ServiceName" class="form-control" readonly="@(!Model.IsNew)" />
|
|
<span asp-validation-for="ServiceName" class="text-danger small"></span>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label asp-for="Key" class="form-label fw-semibold">Key</label>
|
|
<input asp-for="Key" class="form-control" readonly="@(!Model.IsNew)" />
|
|
<span asp-validation-for="Key" class="text-danger small"></span>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label asp-for="LanguageCode" class="form-label fw-semibold">Language</label>
|
|
<input asp-for="LanguageCode" class="form-control" readonly="@(!Model.IsNew)" />
|
|
<span asp-validation-for="LanguageCode" class="text-danger small"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label asp-for="Subject" class="form-label fw-semibold">Subject</label>
|
|
<input asp-for="Subject" class="form-control" />
|
|
<span asp-validation-for="Subject" class="text-danger small"></span>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label asp-for="HtmlBody" class="form-label fw-semibold">HTML Body</label>
|
|
<textarea asp-for="HtmlBody" class="form-control font-monospace" rows="10"></textarea>
|
|
<span asp-validation-for="HtmlBody" class="text-danger small"></span>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label asp-for="TextBody" class="form-label fw-semibold">Text Body</label>
|
|
<textarea asp-for="TextBody" class="form-control font-monospace" rows="5"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label asp-for="VariablesJson" class="form-label fw-semibold">Variables (JSON)</label>
|
|
<textarea asp-for="VariablesJson" class="form-control font-monospace" rows="4"
|
|
placeholder='[{"name":"UserName","required":true}]'></textarea>
|
|
<span asp-validation-for="VariablesJson" class="text-danger small"></span>
|
|
<div class="form-text">JSON array of <code>{"name":"...", "required":true|false}</code></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="preview-pane" role="tabpanel" aria-labelledby="preview-tab" tabindex="0">
|
|
<div class="template-preview-panel mb-3">
|
|
<div class="template-preview-panel-header">
|
|
<div>
|
|
<div class="template-preview-title">Preview</div>
|
|
<div class="template-preview-subtitle">Rendered with sample values from the variable list.</div>
|
|
</div>
|
|
<span id="previewStatus" class="badge text-bg-secondary">Ready</span>
|
|
</div>
|
|
|
|
<div class="template-preview-body">
|
|
<div class="template-preview-source template-preview-section-block">
|
|
<div class="template-preview-section-title">Sample values</div>
|
|
<div id="previewVariables" class="template-preview-variables"></div>
|
|
<div class="form-text mt-2">Change these values to see the rendered output update immediately.</div>
|
|
</div>
|
|
|
|
<div class="template-preview-output">
|
|
<div class="template-preview-section-title">Rendered subject</div>
|
|
<div id="previewSubject" class="template-preview-subject"></div>
|
|
|
|
<div class="template-preview-section-title mt-3">Rendered HTML</div>
|
|
<iframe id="previewHtmlFrame" class="template-preview-frame" title="Email HTML preview"></iframe>
|
|
|
|
<div class="template-preview-section-title mt-3">Rendered text</div>
|
|
<pre id="previewText" class="template-preview-text mb-0"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section FormActions {
|
|
<button type="submit" form="templateForm" class="btn btn-primary">
|
|
<i class="bi bi-floppy me-1"></i> Save
|
|
</button>
|
|
<a href="/admin/templates@(string.IsNullOrWhiteSpace(Model.ServiceNameFilter) && string.IsNullOrWhiteSpace(Model.KeyFilter) ? string.Empty : $"?serviceName={Uri.EscapeDataString(Model.ServiceNameFilter ?? string.Empty)}&key={Uri.EscapeDataString(Model.KeyFilter ?? string.Empty)}")" class="btn btn-secondary">
|
|
<i class="bi bi-x-lg me-1"></i> Cancel
|
|
</a>
|
|
}
|
|
|
|
@section Scripts {
|
|
<script>
|
|
(function () {
|
|
const subjectField = document.getElementById('Subject');
|
|
const htmlField = document.getElementById('HtmlBody');
|
|
const textField = document.getElementById('TextBody');
|
|
const variablesField = document.getElementById('VariablesJson');
|
|
const previewVariablesHost = document.getElementById('previewVariables');
|
|
const previewSubject = document.getElementById('previewSubject');
|
|
const previewText = document.getElementById('previewText');
|
|
const previewFrame = document.getElementById('previewHtmlFrame');
|
|
const previewStatus = document.getElementById('previewStatus');
|
|
const previewTab = document.getElementById('preview-tab');
|
|
|
|
if (!subjectField || !htmlField || !textField || !variablesField || !previewVariablesHost || !previewSubject || !previewText || !previewFrame || !previewStatus) {
|
|
return;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function parseVariables() {
|
|
const raw = variablesField.value?.trim() || '[]';
|
|
const parsed = JSON.parse(raw);
|
|
|
|
if (!Array.isArray(parsed)) {
|
|
throw new Error('Variables JSON must be an array.');
|
|
}
|
|
|
|
return parsed
|
|
.filter(item => item && typeof item.Name === 'string' && item.Name.trim().length > 0)
|
|
.map(item => ({
|
|
name: item.Name.trim(),
|
|
required: !!item.Required
|
|
}));
|
|
}
|
|
|
|
function buildSampleValue(name) {
|
|
return `Sample ${name}`;
|
|
}
|
|
|
|
function renderVariableInputs() {
|
|
let variables = [];
|
|
|
|
try {
|
|
variables = parseVariables();
|
|
previewStatus.className = 'badge text-bg-secondary';
|
|
previewStatus.textContent = 'Ready';
|
|
} catch (error) {
|
|
previewVariablesHost.innerHTML = `<div class="alert alert-warning mb-0">${escapeHtml(error.message || 'Invalid variables JSON')}</div>`;
|
|
previewStatus.className = 'badge text-bg-danger';
|
|
previewStatus.textContent = 'Invalid JSON';
|
|
updatePreview();
|
|
return;
|
|
}
|
|
|
|
if (variables.length === 0) {
|
|
previewVariablesHost.innerHTML = '<div class="text-muted small">No variables defined.</div>';
|
|
updatePreview();
|
|
return;
|
|
}
|
|
|
|
const currentValues = readVariableValues();
|
|
previewVariablesHost.innerHTML = variables.map(variable => {
|
|
const value = Object.prototype.hasOwnProperty.call(currentValues, variable.name)
|
|
? currentValues[variable.name]
|
|
: buildSampleValue(variable.name);
|
|
return `
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold mb-1">${escapeHtml(variable.name)}${variable.required ? ' *' : ''}</label>
|
|
<input type="text" class="form-control form-control-sm preview-variable-input" data-variable-name="${escapeHtml(variable.name)}" value="${escapeHtml(value)}" />
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
previewVariablesHost.querySelectorAll('.preview-variable-input').forEach(input => {
|
|
input.addEventListener('input', updatePreview);
|
|
});
|
|
|
|
updatePreview();
|
|
}
|
|
|
|
function readVariableValues() {
|
|
const values = {};
|
|
previewVariablesHost.querySelectorAll('.preview-variable-input').forEach(input => {
|
|
values[input.dataset.variableName] = input.value;
|
|
});
|
|
return values;
|
|
}
|
|
|
|
function interpolate(text, values) {
|
|
let result = text || '';
|
|
|
|
Object.keys(values).forEach(key => {
|
|
const token = new RegExp(`\\{\\{${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}\\}`, 'g');
|
|
result = result.replace(token, values[key] ?? '');
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function updatePreview() {
|
|
let values = {};
|
|
|
|
try {
|
|
values = readVariableValues();
|
|
} catch (error) {
|
|
values = {};
|
|
}
|
|
|
|
const renderedSubject = interpolate(subjectField.value, values);
|
|
const renderedHtml = interpolate(htmlField.value, values);
|
|
const renderedText = interpolate(textField.value, values);
|
|
|
|
previewSubject.textContent = renderedSubject || '(empty subject)';
|
|
previewText.textContent = renderedText || '(empty text body)';
|
|
previewFrame.srcdoc = `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 24px;
|
|
background: #eef2f7;
|
|
color: #1f2937;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
.email-shell {
|
|
max-width: 640px;
|
|
margin: 0 auto;
|
|
background: #ffffff;
|
|
border: 1px solid #dbe3ee;
|
|
border-radius: 20px;
|
|
overflow: hidden;
|
|
box-shadow: 0 12px 30px rgba(15, 23, 42, .08);
|
|
}
|
|
.email-body {
|
|
padding: 28px 36px;
|
|
}
|
|
img { max-width: 100%; height: auto; }
|
|
a { color: #2563eb; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="email-shell">
|
|
<div class="email-body">
|
|
${renderedHtml || '<div style="color:#6b7280">No HTML body provided.</div>'}
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
variablesField.addEventListener('input', renderVariableInputs);
|
|
subjectField.addEventListener('input', updatePreview);
|
|
htmlField.addEventListener('input', updatePreview);
|
|
textField.addEventListener('input', updatePreview);
|
|
if (previewTab) {
|
|
previewTab.addEventListener('shown.bs.tab', updatePreview);
|
|
}
|
|
|
|
renderVariableInputs();
|
|
})();
|
|
</script>
|
|
}
|
|
</form>
|