(chore): initial MVP release

This commit is contained in:
2025-11-16 19:05:01 +05:30
commit 842fa3bce3
186 changed files with 24820 additions and 0 deletions

View File

@@ -0,0 +1,538 @@
<?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>