docfast/public/app.js
DocFast CEO 432a24dd81
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
fix: download button in playground + de-emphasize rate limits
- Fix download button: exclude #demoDownload from smooth scroll handler
  that was calling preventDefault() on blob: URLs after PDF generation
- Replace '5,000 PDFs per month' with 'High-volume PDF generation' in pricing
- Update schema.org structured data to remove specific limits
2026-02-20 09:51:20 +00:00

492 lines
22 KiB
JavaScript

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: '<html>\n<head>\n<style>\n body { font-family: Inter, -apple-system, sans-serif; padding: 40px; color: #1a1a2e; }\n .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; }\n .company { font-size: 24px; font-weight: 800; color: #34d399; }\n .invoice-title { font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 1px; }\n .invoice-num { font-size: 20px; font-weight: 700; }\n .meta { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 40px; }\n .meta-label { font-size: 12px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }\n .meta-value { font-size: 14px; }\n table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }\n th { background: #f8f9fa; padding: 12px 16px; text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: #666; border-bottom: 2px solid #e9ecef; }\n td { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; font-size: 14px; }\n .text-right { text-align: right; }\n .total-row td { font-weight: 700; font-size: 16px; border-top: 2px solid #1a1a2e; border-bottom: none; }\n .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #e9ecef; font-size: 12px; color: #999; text-align: center; }\n</style>\n</head>\n<body>\n <div class="header">\n <div>\n <div class="company">Acme Corp</div>\n <div style="color:#666;font-size:13px;">123 Business Ave, Suite 100<br>San Francisco, CA 94102</div>\n </div>\n <div style="text-align:right;">\n <div class="invoice-title">Invoice</div>\n <div class="invoice-num">#INV-2024-0042</div>\n <div style="color:#666;font-size:13px;margin-top:4px;">Feb 20, 2026</div>\n </div>\n </div>\n <div class="meta">\n <div><div class="meta-label">Bill To</div><div class="meta-value"><strong>Jane Smith</strong><br>456 Client Road<br>New York, NY 10001</div></div>\n <div><div class="meta-label">Payment Due</div><div class="meta-value">March 20, 2026</div><div class="meta-label" style="margin-top:12px;">Payment Method</div><div class="meta-value">Bank Transfer</div></div>\n </div>\n <table>\n <thead><tr><th>Description</th><th class="text-right">Qty</th><th class="text-right">Rate</th><th class="text-right">Amount</th></tr></thead>\n <tbody>\n <tr><td>Web Development — Landing Page</td><td class="text-right">40 hrs</td><td class="text-right">$150</td><td class="text-right">$6,000</td></tr>\n <tr><td>UI/UX Design — Mockups</td><td class="text-right">16 hrs</td><td class="text-right">$125</td><td class="text-right">$2,000</td></tr>\n <tr><td>API Integration &amp; Testing</td><td class="text-right">24 hrs</td><td class="text-right">$150</td><td class="text-right">$3,600</td></tr>\n <tr class="total-row"><td colspan="3">Total</td><td class="text-right">$11,600</td></tr>\n </tbody>\n </table>\n <div class="footer">Thank you for your business! • Payment terms: Net 30 • Acme Corp</div>\n</body>\n</html>',
report: '<html>\n<head>\n<style>\n body { font-family: Inter, -apple-system, sans-serif; padding: 40px; color: #1a1a2e; line-height: 1.6; }\n h1 { font-size: 28px; font-weight: 800; margin-bottom: 8px; }\n .subtitle { color: #666; font-size: 14px; margin-bottom: 32px; }\n .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 32px; }\n .stat { background: #f8f9fa; border-radius: 12px; padding: 20px; text-align: center; }\n .stat-num { font-size: 32px; font-weight: 800; color: #34d399; }\n .stat-label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }\n h2 { font-size: 18px; font-weight: 700; margin: 28px 0 12px; padding-bottom: 8px; border-bottom: 2px solid #f0f0f0; }\n p { color: #444; font-size: 14px; }\n .highlight { background: linear-gradient(135deg, #34d399 0%, #60a5fa 100%); color: white; border-radius: 12px; padding: 24px; margin: 24px 0; }\n .highlight h3 { font-size: 16px; margin-bottom: 8px; }\n .highlight p { color: rgba(255,255,255,0.9); }\n</style>\n</head>\n<body>\n <h1>Q4 2025 Performance Report</h1>\n <div class="subtitle">Prepared by Analytics Team — February 2026</div>\n <div class="stats">\n <div class="stat"><div class="stat-num">142%</div><div class="stat-label">Revenue Growth</div></div>\n <div class="stat"><div class="stat-num">2.4M</div><div class="stat-label">API Calls</div></div>\n <div class="stat"><div class="stat-num">99.9%</div><div class="stat-label">Uptime</div></div>\n </div>\n <h2>Executive Summary</h2>\n <p>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.</p>\n <div class="highlight">\n <h3>🎯 Key Achievement</h3>\n <p>Crossed 10,000 active users milestone in December, two months ahead of target. Enterprise segment grew 200% QoQ.</p>\n </div>\n <h2>Product Updates</h2>\n <p>Launched 3 major features: batch processing, webhook notifications, and custom templates. Template engine adoption reached 40% of Pro users within the first month.</p>\n <h2>Outlook</h2>\n <p>Q1 2026 focus areas include Markdown-to-PDF improvements, enhanced template library, and SOC 2 certification to unlock enterprise sales pipeline.</p>\n</body>\n</html>',
custom: '<html>\n<head>\n<style>\n body {\n font-family: sans-serif;\n padding: 40px;\n }\n h1 { color: #34d399; }\n</style>\n</head>\n<body>\n <h1>Hello World!</h1>\n <p>Edit this HTML and watch the preview update in real time.</p>\n <p>Then click <strong>Generate PDF</strong> to download it.</p>\n</body>\n</html>'
};
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 (exclude download link)
document.querySelectorAll('a[href^="#"]').forEach(function(a) {
if (a.id === 'demoDownload') return;
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(); }
}
}
});
})();