mirror of
https://github.com/xodivorce/mailer-dev-console.git
synced 2025-12-18 15:22:56 +05:30
539 lines
26 KiB
PHP
539 lines
26 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../mail/mailer.php';
|
||
require_once __DIR__ . '/../mail/templates/loader.php';
|
||
|
||
$selectedTemplate = $_POST['tpl'] ?? $_GET['tpl'] ?? 'quary';
|
||
|
||
$validTemplates = array_keys(mailTemplateMap());
|
||
if (!in_array($selectedTemplate, $validTemplates, true)) {
|
||
$selectedTemplate = 'quary';
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$sent = sendAccountCreationMail(false, $selectedTemplate);
|
||
$code = $sent ? 'ok' : 'fail';
|
||
header('Location: ' . $_SERVER['PHP_SELF'] . '?status=' . $code . '&tpl=' . urlencode($selectedTemplate));
|
||
exit;
|
||
}
|
||
|
||
sendAccountCreationMail(true, $selectedTemplate);
|
||
|
||
$subject = $GLOBALS['MAIL_PREVIEW_SUBJECT'] ?? '(no subject)';
|
||
$body = $GLOBALS['MAIL_PREVIEW_BODY'] ?? '';
|
||
$to = $GLOBALS['MAIL_PREVIEW_TO'] ?? '';
|
||
$username = $GLOBALS['MAIL_PREVIEW_USERNAME'] ?? '';
|
||
$domain = $GLOBALS['MAIL_PREVIEW_DOMAIN'] ?? '';
|
||
$company = $GLOBALS['MAIL_PREVIEW_COMPANY'] ?? '';
|
||
$github = $GLOBALS['MAIL_PREVIEW_GITHUB'] ?? '';
|
||
$website = $GLOBALS['MAIL_PREVIEW_WEBSITE'] ?? '';
|
||
$fromEmail = $GLOBALS['MAIL_PREVIEW_FROM'] ?? '';
|
||
$fromName = $GLOBALS['MAIL_PREVIEW_FROM_NAME'] ?? '';
|
||
|
||
$smtpHost = $GLOBALS['MAIL_PREVIEW_MAIL_HOST'] ?? '';
|
||
$smtpUser = $GLOBALS['MAIL_PREVIEW_MAIL_USERNAME'] ?? '';
|
||
$smtpPort = $GLOBALS['MAIL_PREVIEW_MAIL_PORT'] ?? '';
|
||
$smtpEncryption = $GLOBALS['MAIL_PREVIEW_MAIL_ENCRYPTION'] ?? '';
|
||
$smtpPassword = $GLOBALS['MAIL_PREVIEW_MAIL_PASSWORD'] ?? '';
|
||
$projectStart = $GLOBALS['MAIL_PREVIEW_PROJECT_START'] ?? '';
|
||
$currentYear = $GLOBALS['MAIL_PREVIEW_CURRENT_YEAR'] ?? '';
|
||
|
||
$templateIndex = $GLOBALS['MAIL_PREVIEW_TEMPLATES_INDEX'] ?? [];
|
||
$templateLabel = $GLOBALS['MAIL_PREVIEW_TEMPLATE_LABEL'] ?? '';
|
||
$templateKey = $GLOBALS['MAIL_PREVIEW_TEMPLATE_KEY'] ?? $selectedTemplate;
|
||
|
||
/**
|
||
* Build a full state snapshot so ANY change (template or mail config)
|
||
* will change the hash and trigger a live refresh.
|
||
*/
|
||
$previewState = [
|
||
'body' => $body,
|
||
'subject' => $subject,
|
||
'to' => $to,
|
||
'username' => $username,
|
||
'domain' => $domain,
|
||
'company' => $company,
|
||
'fromName' => $fromName,
|
||
'fromEmail' => $fromEmail,
|
||
'smtpHost' => $smtpHost,
|
||
'smtpUser' => $smtpUser,
|
||
'smtpPort' => $smtpPort,
|
||
'smtpEncryption' => $smtpEncryption,
|
||
// we only care if a password exists, not the value itself
|
||
'hasSmtpPassword' => (bool) $smtpPassword,
|
||
'projectStart' => $projectStart,
|
||
'currentYear' => $currentYear,
|
||
];
|
||
|
||
$bodyHash = sha1(json_encode($previewState, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||
|
||
if (isset($_GET['mode']) && $_GET['mode'] === 'json') {
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
echo json_encode([
|
||
'hash' => $bodyHash,
|
||
'state' => $previewState,
|
||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Status banner state
|
||
*/
|
||
$statusKind = null; // 'success' | 'error' | null
|
||
$statusMessage = null;
|
||
|
||
if (isset($_GET['status'])) {
|
||
if ($_GET['status'] === 'ok') {
|
||
$statusKind = 'success';
|
||
$statusMessage = "Email sent successfully to {$to}";
|
||
} elseif ($_GET['status'] === 'fail') {
|
||
$statusKind = 'error';
|
||
$statusMessage = "Failed to send email. Check error logs.";
|
||
}
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>Mail Renderer - Xovae</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
|
||
<link href="https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@300;400;500;600;700&display=swap"
|
||
rel="stylesheet" />
|
||
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
fontFamily: {
|
||
sans: ['"Lexend Deca"', 'system-ui', 'sans-serif'],
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.grid-pattern {
|
||
background-image:
|
||
linear-gradient(to right, rgba(148, 163, 184, 0.08) 1px, transparent 1px),
|
||
linear-gradient(to bottom, rgba(148, 163, 184, 0.08) 1px, transparent 1px);
|
||
background-size: 24px 24px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body class="bg-slate-50 text-slate-900 min-h-screen font-sans antialiased">
|
||
<div class="min-h-screen flex flex-col">
|
||
<header class="bg-slate-900 text-white border-b border-slate-700">
|
||
<div class="max-w-[1800px] mx-auto px-6 py-6">
|
||
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||
<div class="flex items-center gap-4">
|
||
<div
|
||
class="flex items-center justify-center w-12 h-12 bg-blue-600 text-white font-bold text-lg rounded-md">
|
||
<?= strtoupper(substr($domain ?: 'UD', 0, 2)) ?>
|
||
</div>
|
||
<div>
|
||
<h1 class="text-2xl font-semibold tracking-tight" id="domainTitle">
|
||
<?= htmlspecialchars($domain ?: 'MAIL PREVIEW CONSOLE') ?>
|
||
</h1>
|
||
<p class="text-xs font-medium text-slate-400 tracking-[0.18em] mt-0.5">
|
||
Mail Template Review & Delivery Workspace
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||
<form method="get"
|
||
class="flex items-center gap-3 bg-slate-800/90 border border-slate-700 px-4 py-2.5 rounded-lg">
|
||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||
TEMPLATE
|
||
</label>
|
||
<div class="relative">
|
||
<select name="tpl" class="appearance-none bg-slate-700 border border-slate-600 text-white text-sm
|
||
pl-3 pr-8 py-1.5 rounded-md
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||
cursor-pointer" onchange="this.form.submit()">
|
||
<?php foreach ($templateIndex as $tpl): ?>
|
||
<option value="<?= htmlspecialchars($tpl['key']) ?>"
|
||
<?= $tpl['key'] === $templateKey ? 'selected' : '' ?>>
|
||
<?= htmlspecialchars($tpl['label']) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<svg class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300"
|
||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</div>
|
||
</form>
|
||
|
||
<form method="post" onsubmit="handleSendEmail(event)">
|
||
<input type="hidden" name="tpl" value="<?= htmlspecialchars($templateKey) ?>" />
|
||
<button type="submit" name="send" id="sendButton" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 rounded font-medium text-sm
|
||
transition-colors focus:outline-none focus:ring-2
|
||
focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-slate-900
|
||
flex items-center gap-2">
|
||
<svg id="sendIcon" class="w-4 h-4" fill="none" stroke="currentColor"
|
||
viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||
</svg>
|
||
<svg id="loadingIcon" class="w-4 h-4 animate-spin hidden" fill="none"
|
||
stroke="currentColor" viewBox="0 0 24 24">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke-width="4" />
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0
|
||
3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||
</svg>
|
||
<span id="sendText">Send Email</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<?php if ($statusKind && $statusMessage): ?>
|
||
<div class="bg-white border-b border-slate-200">
|
||
<div class="max-w-[1800px] mx-auto px-6 py-3">
|
||
<div
|
||
class="flex items-center gap-3 <?= $statusKind === 'success' ? 'text-green-700' : 'text-red-700' ?>">
|
||
<div
|
||
class="flex items-center justify-center w-8 h-8 rounded-full <?= $statusKind === 'success' ? 'bg-green-100' : 'bg-red-100' ?>">
|
||
<span class="text-lg font-bold">
|
||
<?= $statusKind === 'success' ? '✓' : '✕' ?>
|
||
</span>
|
||
</div>
|
||
<p class="font-medium text-sm"><?= htmlspecialchars($statusMessage) ?></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="flex-1 max-w-[1800px] mx-auto w-full px-6 py-6">
|
||
<div class="grid grid-cols-1 xl:grid-cols-[380px,minmax(0,1fr)] gap-6 h-full">
|
||
<section class="bg-white border border-slate-200 shadow-sm rounded-lg overflow-hidden flex flex-col">
|
||
<div class="bg-slate-800 text-white px-5 py-4 border-b border-slate-700">
|
||
<h2 class="font-semibold text-sm tracking-wider">
|
||
Message Configurations From .env
|
||
</h2>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto px-5 py-5 space-y-6 text-sm">
|
||
<div>
|
||
<h3
|
||
class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3 pb-2 border-b border-slate-200">
|
||
Message Details
|
||
</h3>
|
||
<dl class="space-y-3">
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
From
|
||
</dt>
|
||
<dd class="text-slate-900">
|
||
<div class="font-medium" id="fromNameValue">
|
||
<?= htmlspecialchars($fromName ?: 'N/A') ?>
|
||
</div>
|
||
<div class="text-xs text-slate-500" id="fromEmailValue">
|
||
<?= htmlspecialchars($fromEmail ?: 'N/A') ?>
|
||
</div>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
To
|
||
</dt>
|
||
<dd class="text-slate-900">
|
||
<div class="font-medium" id="toNameValue">
|
||
<?= htmlspecialchars($username ?: 'User') ?>
|
||
</div>
|
||
<div class="text-xs text-slate-500" id="toEmailValue">
|
||
<?= htmlspecialchars($to ?: 'N/A') ?>
|
||
</div>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Subject
|
||
</dt>
|
||
<dd class="font-medium text-slate-900" id="subjectValue">
|
||
<?= htmlspecialchars($subject) ?>
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
|
||
<div>
|
||
<h3
|
||
class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3 pb-2 border-b border-slate-200">
|
||
Organization
|
||
</h3>
|
||
<dl class="space-y-3">
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Domain
|
||
</dt>
|
||
<dd class="font-medium text-slate-900" id="domainValue">
|
||
<?= htmlspecialchars($domain ?: '—') ?>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Company
|
||
</dt>
|
||
<dd class="font-medium text-slate-900" id="companyValue">
|
||
<?= htmlspecialchars($company ?: '—') ?>
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
|
||
<div>
|
||
<h3
|
||
class="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3 pb-2 border-slate-200 border-b">
|
||
SMTP Configuration
|
||
</h3>
|
||
<dl class="space-y-3">
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Host
|
||
</dt>
|
||
<dd class="text-xs text-slate-900" id="smtpHostValue">
|
||
<?= htmlspecialchars($smtpHost ?: '—') ?>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Username
|
||
</dt>
|
||
<dd class="text-xs text-slate-900" id="smtpUserValue">
|
||
<?= htmlspecialchars($smtpUser ?: '—') ?>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Port
|
||
</dt>
|
||
<dd class="text-xs text-slate-900" id="smtpPortValue">
|
||
<?= htmlspecialchars($smtpPort ?: '—') ?>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Encryption
|
||
</dt>
|
||
<dd class="text-xs text-slate-900" id="smtpEncValue">
|
||
<?= htmlspecialchars($smtpEncryption ?: '—') ?>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Password
|
||
</dt>
|
||
<dd class="text-xs text-slate-500" id="smtpPwdValue">
|
||
<?= htmlspecialchars($smtpPassword ? '••••••••' : '—') ?>
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">
|
||
Project Timeline
|
||
</dt>
|
||
<dd class="text-xs text-slate-900" id="projectTimelineValue">
|
||
<?php if ($projectStart && $currentYear): ?>
|
||
<?= htmlspecialchars($projectStart) ?> – <?= htmlspecialchars($currentYear) ?>
|
||
<?php else: ?>
|
||
<span class="text-slate-400">—</span>
|
||
<?php endif; ?>
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
|
||
<div class="bg-slate-50 border border-slate-200 rounded p-4">
|
||
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-700 mb-2">
|
||
Documentation
|
||
</h3>
|
||
<p class="text-xs leading-relaxed text-slate-600">
|
||
Preview live-rendered HTML email templates are generated by PHPMailer.
|
||
To modify a template, edit
|
||
<code class="text-[10px] bg-white px-1.5 py-0.5 border border-slate-300 rounded">
|
||
app/mail/mailer.php
|
||
</code>
|
||
and manage template definitions in
|
||
<code class="text-[10px] bg-white px-1.5 py-0.5 border border-slate-300 rounded">
|
||
app/mail/templates/loader.php
|
||
</code>.
|
||
For more, visit
|
||
<a href="https://github.com/xodivorce/mailer-dev-console" target="_blank"
|
||
class="text-[#0f172a] hover:text-[#0f172acb] underline">
|
||
xodivorce/mailer-dev-console
|
||
</a>.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section
|
||
class="bg-white border border-slate-200 shadow-sm rounded-lg overflow-hidden flex flex-col min-h-[700px]">
|
||
<div class="bg-slate-100 border-b border-slate-200 px-5 py-4">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<div id="livePreviewBadge"
|
||
class="flex items-center justify-center w-10 h-8 bg-slate-700 text-white text-xs rounded transition-colors duration-200">
|
||
HTML
|
||
</div>
|
||
<div>
|
||
<h2 class="font-semibold text-slate-900 text-base">
|
||
Rendering Console
|
||
</h2>
|
||
<p id="previewSubjectLine" class="text-xs text-slate-500 mt-0.5 leading-snug">
|
||
<?= htmlspecialchars($subject) ?>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs font-semibold text-slate-600 tracking-wider">
|
||
iFRAME RENDERING..
|
||
</span>
|
||
<span class="relative flex h-2 w-2">
|
||
<span
|
||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex-1 grid-pattern bg-slate-50 p-2">
|
||
<iframe id="previewFrame"
|
||
class="w-full h-full bg-white border border-slate-200 rounded shadow-inner"
|
||
srcdoc="<?= htmlspecialchars($body, ENT_QUOTES, 'UTF-8') ?>"></iframe>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const MAILER_INITIAL_HASH = "<?= htmlspecialchars($bodyHash, ENT_QUOTES, 'UTF-8') ?>";
|
||
const MAILER_TPL_KEY = "<?= htmlspecialchars($templateKey, ENT_QUOTES, 'UTF-8') ?>";
|
||
const MAILER_PHP_SELF = "<?= htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') ?>";
|
||
|
||
let currentHash = MAILER_INITIAL_HASH;
|
||
|
||
function handleSendEmail(event) {
|
||
const button = document.getElementById('sendButton');
|
||
const sendIcon = document.getElementById('sendIcon');
|
||
const loadingIcon = document.getElementById('loadingIcon');
|
||
const sendText = document.getElementById('sendText');
|
||
|
||
button.disabled = true;
|
||
button.classList.add('opacity-75', 'cursor-not-allowed');
|
||
button.classList.remove('hover:bg-blue-700');
|
||
|
||
sendIcon.classList.add('hidden');
|
||
loadingIcon.classList.remove('hidden');
|
||
sendText.textContent = 'Processing...';
|
||
}
|
||
|
||
function flashLivePreviewBadge() {
|
||
const badge = document.getElementById('livePreviewBadge');
|
||
if (!badge) return;
|
||
badge.classList.remove('bg-slate-700');
|
||
badge.classList.add('bg-green-600');
|
||
setTimeout(() => {
|
||
badge.classList.remove('bg-green-600');
|
||
badge.classList.add('bg-slate-700');
|
||
}, 600);
|
||
}
|
||
|
||
function setText(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (el) {
|
||
el.textContent = value;
|
||
}
|
||
}
|
||
|
||
async function pollTemplateChanges() {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
tpl: MAILER_TPL_KEY,
|
||
mode: 'json',
|
||
_: Date.now().toString(),
|
||
});
|
||
|
||
const res = await fetch(MAILER_PHP_SELF + '?' + params.toString(), {
|
||
cache: 'no-store',
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Preview poll failed');
|
||
|
||
const data = await res.json();
|
||
if (!data || !data.hash || !data.state) return;
|
||
|
||
if (data.hash !== currentHash) {
|
||
currentHash = data.hash;
|
||
|
||
const s = data.state;
|
||
|
||
// iframe + subject
|
||
const iframe = document.getElementById('previewFrame');
|
||
const subjectLine = document.getElementById('previewSubjectLine');
|
||
|
||
if (iframe && typeof s.body === 'string') {
|
||
iframe.srcdoc = s.body;
|
||
}
|
||
if (subjectLine && typeof s.subject === 'string') {
|
||
subjectLine.textContent = s.subject || '(no subject)';
|
||
}
|
||
setText('subjectValue', s.subject || '(no subject)');
|
||
|
||
// message details
|
||
setText('fromNameValue', s.fromName || 'N/A');
|
||
setText('fromEmailValue', s.fromEmail || 'N/A');
|
||
setText('toNameValue', s.username || 'User');
|
||
setText('toEmailValue', s.to || 'N/A');
|
||
|
||
// org
|
||
setText('domainValue', s.domain || '—');
|
||
setText('companyValue', s.company || '—');
|
||
|
||
// header title domain
|
||
const domainTitle = document.getElementById('domainTitle');
|
||
if (domainTitle) {
|
||
domainTitle.textContent = s.domain || 'MAIL PREVIEW CONSOLE';
|
||
}
|
||
|
||
// smtp
|
||
setText('smtpHostValue', s.smtpHost || '—');
|
||
setText('smtpUserValue', s.smtpUser || '—');
|
||
setText('smtpPortValue', s.smtpPort || '—');
|
||
setText('smtpEncValue', s.smtpEncryption || '—');
|
||
setText('smtpPwdValue', s.hasSmtpPassword ? '••••••••' : '—');
|
||
|
||
// project timeline
|
||
const timeline = (s.projectStart && s.currentYear)
|
||
? (s.projectStart + ' – ' + s.currentYear)
|
||
: '—';
|
||
setText('projectTimelineValue', timeline);
|
||
|
||
flashLivePreviewBadge();
|
||
}
|
||
} catch (err) {
|
||
// silent fail, retry
|
||
} finally {
|
||
setTimeout(pollTemplateChanges, 2000);
|
||
}
|
||
}
|
||
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
pollTemplateChanges();
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|