var recoverEmail = ''; function showRecoverState(state) { ['recoverInitial', 'recoverLoading', 'recoverVerify', 'recoverResult'].forEach(function(id) { var el = document.getElementById(id); if (el) el.classList.remove('active'); }); document.getElementById(state).classList.add('active'); } function openRecover() { document.getElementById('recoverModal').classList.add('active'); showRecoverState('recoverInitial'); var errEl = document.getElementById('recoverError'); if (errEl) errEl.style.display = 'none'; var verifyErrEl = document.getElementById('recoverVerifyError'); if (verifyErrEl) verifyErrEl.style.display = 'none'; document.getElementById('recoverEmailInput').value = ''; document.getElementById('recoverCode').value = ''; recoverEmail = ''; setTimeout(function() { document.getElementById('recoverEmailInput').focus(); }, 100); } function closeRecover() { document.getElementById('recoverModal').classList.remove('active'); } async function submitRecover() { var errEl = document.getElementById('recoverError'); var btn = document.getElementById('recoverBtn'); var emailInput = document.getElementById('recoverEmailInput'); var email = emailInput.value.trim(); if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { errEl.textContent = 'Please enter a valid email address.'; errEl.style.display = 'block'; return; } errEl.style.display = 'none'; btn.disabled = true; showRecoverState('recoverLoading'); try { var res = await fetch('/v1/recover', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }); var data = await res.json(); if (!res.ok) { showRecoverState('recoverInitial'); errEl.textContent = data.error || 'Something went wrong.'; errEl.style.display = 'block'; btn.disabled = false; return; } recoverEmail = email; document.getElementById('recoverEmailDisplay').textContent = email; showRecoverState('recoverVerify'); document.getElementById('recoverCode').focus(); btn.disabled = false; } catch (err) { showRecoverState('recoverInitial'); errEl.textContent = 'Network error. Please try again.'; errEl.style.display = 'block'; btn.disabled = false; } } async function submitRecoverVerify() { var errEl = document.getElementById('recoverVerifyError'); var btn = document.getElementById('recoverVerifyBtn'); var codeInput = document.getElementById('recoverCode'); var code = codeInput.value.trim(); if (!code || !/^\d{6}$/.test(code)) { errEl.textContent = 'Please enter a 6-digit code.'; errEl.style.display = 'block'; return; } errEl.style.display = 'none'; btn.disabled = true; try { var res = await fetch('/v1/recover/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: recoverEmail, code: code }) }); var data = await res.json(); if (!res.ok) { errEl.textContent = data.error || 'Verification failed.'; errEl.style.display = 'block'; btn.disabled = false; return; } if (data.apiKey) { document.getElementById('recoveredKeyText').textContent = data.apiKey; showRecoverState('recoverResult'); var rH2 = document.querySelector('#recoverResult h2'); if (rH2) { rH2.setAttribute('tabindex', '-1'); rH2.focus(); } } else { errEl.textContent = data.message || 'No key found for this email.'; errEl.style.display = 'block'; btn.disabled = false; } } catch (err) { errEl.textContent = 'Network error. Please try again.'; errEl.style.display = 'block'; btn.disabled = false; } } function copyRecoveredKey() { var key = document.getElementById('recoveredKeyText').textContent; var btn = document.getElementById('copyRecoveredBtn'); doCopy(key, btn); } function doCopy(text, btn) { function showCopied() { btn.textContent = '\u2713 Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 2000); } function showFailed() { btn.textContent = 'Failed'; setTimeout(function() { btn.textContent = 'Copy'; }, 2000); } try { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(showCopied).catch(function() { fallbackCopy(text, showCopied, showFailed); }); } else { fallbackCopy(text, showCopied, showFailed); } } catch(e) { showFailed(); } } function fallbackCopy(text, onSuccess, onFail) { try { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; ta.style.top = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); var success = document.execCommand('copy'); document.body.removeChild(ta); success ? onSuccess() : onFail(); } catch(e) { onFail(); } } async function checkout() { try { var res = await fetch('/v1/billing/checkout', { method: 'POST' }); var data = await res.json(); if (data.url) window.location.href = data.url; else alert('Checkout is not available yet. Please try again later.'); } catch (err) { alert('Something went wrong. Please try again.'); } } // === Demo Playground === var pgTemplates = { invoice: '\n\n\n\n\n
\n
\n
Acme Corp
\n
123 Business Ave, Suite 100
San Francisco, CA 94102
\n
\n
\n
Invoice
\n
#INV-2024-0042
\n
Feb 20, 2026
\n
\n
\n
\n
Bill To
Jane Smith
456 Client Road
New York, NY 10001
\n
Payment Due
March 20, 2026
Payment Method
Bank Transfer
\n
\n \n \n \n \n \n \n \n \n
DescriptionQtyRateAmount
Web Development — Landing Page40 hrs$150$6,000
UI/UX Design — Mockups16 hrs$125$2,000
API Integration & Testing24 hrs$150$3,600
Total$11,600
\n \n\n', report: '\n\n\n\n\n

Q4 2025 Performance Report

\n
Prepared by Analytics Team — February 2026
\n
\n
142%
Revenue Growth
\n
2.4M
API Calls
\n
99.9%
Uptime
\n
\n

Executive Summary

\n

Q4 saw exceptional growth across all key metrics. Customer acquisition increased by 85% while churn decreased to an all-time low of 1.2%. Our expansion into the EU market contributed significantly to revenue gains.

\n
\n

🎯 Key Achievement

\n

Crossed 10,000 active users milestone in December, two months ahead of target. Enterprise segment grew 200% QoQ.

\n
\n

Product Updates

\n

Launched 3 major features: batch processing, webhook notifications, and custom templates. Template engine adoption reached 40% of Pro users within the first month.

\n

Outlook

\n

Q1 2026 focus areas include Markdown-to-PDF improvements, enhanced template library, and SOC 2 certification to unlock enterprise sales pipeline.

\n\n', custom: '\n\n\n\n\n

Hello World!

\n

Edit this HTML and watch the preview update in real time.

\n

Then click Generate PDF to download it.

\n\n' }; var previewDebounce = null; function updatePreview() { var iframe = document.getElementById('demoPreview'); var html = document.getElementById('demoHtml').value; if (!iframe) return; var doc = iframe.contentDocument || iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); } function setTemplate(name) { var ta = document.getElementById('demoHtml'); ta.value = pgTemplates[name] || pgTemplates.custom; updatePreview(); // Update active tab document.querySelectorAll('.pg-tab').forEach(function(t) { var isActive = t.getAttribute('data-template') === name; t.classList.toggle('active', isActive); t.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); } async function generateDemo() { var btn = document.getElementById('demoGenerateBtn'); var status = document.getElementById('demoStatus'); var result = document.getElementById('demoResult'); var errorEl = document.getElementById('demoError'); var html = document.getElementById('demoHtml').value.trim(); if (!html) { errorEl.textContent = 'Please enter some HTML.'; errorEl.style.display = 'block'; result.classList.remove('visible'); return; } errorEl.style.display = 'none'; result.classList.remove('visible'); btn.disabled = true; btn.classList.add('pg-generating'); status.textContent = 'Generating…'; var startTime = performance.now(); try { var res = await fetch('/v1/demo/html', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html: html }) }); if (!res.ok) { var data = await res.json(); errorEl.textContent = data.error || 'Something went wrong.'; errorEl.style.display = 'block'; btn.disabled = false; btn.classList.remove('pg-generating'); status.textContent = ''; return; } var elapsed = ((performance.now() - startTime) / 1000).toFixed(1); var blob = await res.blob(); var url = URL.createObjectURL(blob); document.getElementById('demoDownload').href = url; document.getElementById('demoTime').textContent = elapsed; result.classList.add('visible'); status.textContent = ''; btn.disabled = false; btn.classList.remove('pg-generating'); } catch (err) { errorEl.textContent = 'Network error. Please try again.'; errorEl.style.display = 'block'; btn.disabled = false; btn.classList.remove('pg-generating'); status.textContent = ''; } } // === Init === document.addEventListener('DOMContentLoaded', function() { // BUG-068: Open change email modal if navigated via hash if (window.location.hash === '#change-email') { openEmailChange(); } // Demo playground document.getElementById('demoGenerateBtn').addEventListener('click', generateDemo); // Playground tabs document.querySelectorAll('.pg-tab').forEach(function(tab) { tab.addEventListener('click', function() { setTemplate(this.getAttribute('data-template')); }); }); // Init with invoice template setTemplate('invoice'); // Live preview on input document.getElementById('demoHtml').addEventListener('input', function() { clearTimeout(previewDebounce); previewDebounce = setTimeout(updatePreview, 150); }); // Playground checkout button var pgCheckout = document.getElementById('btn-checkout-playground'); if (pgCheckout) pgCheckout.addEventListener('click', checkout); // Checkout buttons document.getElementById('btn-checkout').addEventListener('click', checkout); var heroCheckout = document.getElementById('btn-checkout-hero'); if (heroCheckout) heroCheckout.addEventListener('click', checkout); // Recovery modal document.getElementById('btn-close-recover').addEventListener('click', closeRecover); document.getElementById('recoverBtn').addEventListener('click', submitRecover); document.getElementById('recoverVerifyBtn').addEventListener('click', submitRecoverVerify); document.getElementById('copyRecoveredBtn').addEventListener('click', copyRecoveredKey); document.getElementById('recoverModal').addEventListener('click', function(e) { if (e.target === this) closeRecover(); }); // Open recovery from links document.querySelectorAll('.open-recover').forEach(function(el) { el.addEventListener('click', function(e) { e.preventDefault(); openRecover(); }); }); // Smooth scroll for hash links document.querySelectorAll('a[href^="#"]').forEach(function(a) { a.addEventListener('click', function(e) { var target = this.getAttribute('href'); if (target === '#') return; e.preventDefault(); var el = document.querySelector(target); if (el) el.scrollIntoView({ behavior: 'smooth' }); }); }); }); // --- Email Change --- var emailChangeApiKey = ''; var emailChangeNewEmail = ''; function showEmailChangeState(state) { ['emailChangeInitial', 'emailChangeLoading', 'emailChangeVerify', 'emailChangeResult'].forEach(function(id) { var el = document.getElementById(id); if (el) el.classList.remove('active'); }); document.getElementById(state).classList.add('active'); } function openEmailChange() { closeRecover(); document.getElementById('emailChangeModal').classList.add('active'); showEmailChangeState('emailChangeInitial'); var errEl = document.getElementById('emailChangeError'); if (errEl) errEl.style.display = 'none'; var verifyErrEl = document.getElementById('emailChangeVerifyError'); if (verifyErrEl) verifyErrEl.style.display = 'none'; document.getElementById('emailChangeApiKey').value = ''; document.getElementById('emailChangeNewEmail').value = ''; document.getElementById('emailChangeCode').value = ''; emailChangeApiKey = ''; emailChangeNewEmail = ''; } function closeEmailChange() { document.getElementById('emailChangeModal').classList.remove('active'); } async function submitEmailChange() { var errEl = document.getElementById('emailChangeError'); var btn = document.getElementById('emailChangeBtn'); var apiKey = document.getElementById('emailChangeApiKey').value.trim(); var newEmail = document.getElementById('emailChangeNewEmail').value.trim(); if (!apiKey) { errEl.textContent = 'Please enter your API key.'; errEl.style.display = 'block'; return; } if (!newEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) { errEl.textContent = 'Please enter a valid email address.'; errEl.style.display = 'block'; return; } errEl.style.display = 'none'; btn.disabled = true; showEmailChangeState('emailChangeLoading'); try { var res = await fetch('/v1/email-change', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: apiKey, newEmail: newEmail }) }); var data = await res.json(); if (!res.ok) { showEmailChangeState('emailChangeInitial'); errEl.textContent = data.error || 'Something went wrong.'; errEl.style.display = 'block'; btn.disabled = false; return; } emailChangeApiKey = apiKey; emailChangeNewEmail = newEmail; document.getElementById('emailChangeEmailDisplay').textContent = newEmail; showEmailChangeState('emailChangeVerify'); document.getElementById('emailChangeCode').focus(); btn.disabled = false; } catch (err) { showEmailChangeState('emailChangeInitial'); errEl.textContent = 'Network error. Please try again.'; errEl.style.display = 'block'; btn.disabled = false; } } async function submitEmailChangeVerify() { var errEl = document.getElementById('emailChangeVerifyError'); var btn = document.getElementById('emailChangeVerifyBtn'); var code = document.getElementById('emailChangeCode').value.trim(); if (!code || !/^\d{6}$/.test(code)) { errEl.textContent = 'Please enter a 6-digit code.'; errEl.style.display = 'block'; return; } errEl.style.display = 'none'; btn.disabled = true; try { var res = await fetch('/v1/email-change/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: emailChangeApiKey, newEmail: emailChangeNewEmail, code: code }) }); var data = await res.json(); if (!res.ok) { errEl.textContent = data.error || 'Verification failed.'; errEl.style.display = 'block'; btn.disabled = false; return; } document.getElementById('emailChangeNewDisplay').textContent = data.newEmail || emailChangeNewEmail; showEmailChangeState('emailChangeResult'); var ecH2 = document.querySelector('#emailChangeResult h2'); if (ecH2) { ecH2.setAttribute('tabindex', '-1'); ecH2.focus(); } } catch (err) { errEl.textContent = 'Network error. Please try again.'; errEl.style.display = 'block'; btn.disabled = false; } } // Email change event listeners document.addEventListener('DOMContentLoaded', function() { var closeBtn = document.getElementById('btn-close-email-change'); if (closeBtn) closeBtn.addEventListener('click', closeEmailChange); var changeBtn = document.getElementById('emailChangeBtn'); if (changeBtn) changeBtn.addEventListener('click', submitEmailChange); var verifyBtn = document.getElementById('emailChangeVerifyBtn'); if (verifyBtn) verifyBtn.addEventListener('click', submitEmailChangeVerify); var modal = document.getElementById('emailChangeModal'); if (modal) modal.addEventListener('click', function(e) { if (e.target === this) closeEmailChange(); }); document.querySelectorAll('.open-email-change').forEach(function(el) { el.addEventListener('click', function(e) { e.preventDefault(); openEmailChange(); }); }); }); // === Accessibility: Escape key closes modals, focus trapping === (function() { function getActiveModal() { var modals = ['recoverModal', 'emailChangeModal']; for (var i = 0; i < modals.length; i++) { var m = document.getElementById(modals[i]); if (m && m.classList.contains('active')) return m; } return null; } function closeActiveModal() { var m = getActiveModal(); if (!m) return; m.classList.remove('active'); } document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeActiveModal(); if (e.key === 'Tab') { var modal = getActiveModal(); if (!modal) return; var focusable = modal.querySelectorAll('button:not([disabled]), input:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])'); if (focusable.length === 0) return; var first = focusable[0], last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } }); })();