diff --git a/package-lock.json b/package-lock.json index b08c3b6..49e2634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "pino": "^10.3.1", "puppeteer": "^24.0.0", "stripe": "^20.3.1", - "swagger-jsdoc": "^6.2.8", + "swagger-jsdoc": "^7.0.0-rc.6", "swagger-ui-dist": "^5.31.0" }, "devDependencies": { @@ -63,9 +63,9 @@ "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz", + "integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.6", @@ -73,7 +73,7 @@ "@apidevtools/swagger-methods": "^3.0.2", "@jsdevtools/ono": "^7.1.3", "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" + "z-schema": "^4.2.3" }, "peerDependencies": { "openapi-types": ">=7" @@ -1713,7 +1713,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/compressible": { @@ -2853,9 +2853,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", + "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4002,34 +4002,21 @@ } }, "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "version": "7.0.0-rc.6", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-7.0.0-rc.6.tgz", + "integrity": "sha512-LIvIPQxipRaOzIij+HrWOcCWTINE6OeJuqmXCfDkofVcstPVABHRkaAc3D7vrX9s7L0ccH0sH0amwHgN6+SXPg==", "license": "MIT", "dependencies": { - "commander": "6.2.0", "doctrine": "3.0.0", "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", + "lodash.mergewith": "4.6.2", + "swagger-parser": "10.0.2", "yaml": "2.0.0-1" }, - "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" - }, "engines": { "node": ">=12.0.0" } }, - "node_modules/swagger-jsdoc/node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/swagger-jsdoc/node_modules/yaml": { "version": "2.0.0-1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", @@ -4040,12 +4027,12 @@ } }, "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.2.tgz", + "integrity": "sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==", "license": "MIT", "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" + "@apidevtools/swagger-parser": "10.0.2" }, "engines": { "node": ">=10" @@ -4657,33 +4644,23 @@ } }, "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.4.tgz", + "integrity": "sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==", "license": "MIT", "dependencies": { "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" + "validator": "^13.6.0" }, "bin": { "z-schema": "bin/z-schema" }, "engines": { - "node": ">=8.0.0" + "node": ">=6.0.0" }, "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" + "commander": "^2.7.1" } }, "node_modules/zod": { diff --git a/package.json b/package.json index e9db8d0..bffa2a5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "pino": "^10.3.1", "puppeteer": "^24.0.0", "stripe": "^20.3.1", - "swagger-jsdoc": "^6.2.8", + "swagger-jsdoc": "^7.0.0-rc.6", "swagger-ui-dist": "^5.31.0" }, "devDependencies": { diff --git a/public/app.js b/public/app.js index 808c0e3..86e9023 100644 --- a/public/app.js +++ b/public/app.js @@ -1,492 +1 @@ -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| Description | Qty | Rate | Amount |
|---|---|---|---|
| Web Development — Landing Page | 40 hrs | $150 | $6,000 |
| UI/UX Design — Mockups | 16 hrs | $125 | $2,000 |
| API Integration & Testing | 24 hrs | $150 | $3,600 |
| Total | $11,600 | ||
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.
\nCrossed 10,000 active users milestone in December, two months ahead of target. Enterprise segment grew 200% QoQ.
\nLaunched 3 major features: batch processing, webhook notifications, and custom templates. Template engine adoption reached 40% of Pro users within the first month.
\nQ1 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\nEdit this HTML and watch the preview update in real time.
\nThen 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 (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(); } - } - } - }); -})(); +var recoverEmail="";function showRecoverState(e){["recoverInitial","recoverLoading","recoverVerify","recoverResult"].forEach(function(e){var t=document.getElementById(e);t&&t.classList.remove("active")}),document.getElementById(e).classList.add("active")}function openRecover(){document.getElementById("recoverModal").classList.add("active"),showRecoverState("recoverInitial");var e=document.getElementById("recoverError");e&&(e.style.display="none");var t=document.getElementById("recoverVerifyError");t&&(t.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 e=document.getElementById("recoverError"),t=document.getElementById("recoverBtn"),n=document.getElementById("recoverEmailInput").value.trim();if(!n||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(n))return e.textContent="Please enter a valid email address.",void(e.style.display="block");e.style.display="none",t.disabled=!0,showRecoverState("recoverLoading");try{var a=await fetch("/v1/recover",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:n})}),o=await a.json();if(!a.ok)return showRecoverState("recoverInitial"),e.textContent=o.error||"Something went wrong.",e.style.display="block",void(t.disabled=!1);recoverEmail=n,document.getElementById("recoverEmailDisplay").textContent=n,showRecoverState("recoverVerify"),document.getElementById("recoverCode").focus(),t.disabled=!1}catch(n){showRecoverState("recoverInitial"),e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}async function submitRecoverVerify(){var e=document.getElementById("recoverVerifyError"),t=document.getElementById("recoverVerifyBtn"),n=document.getElementById("recoverCode").value.trim();if(!n||!/^\d{6}$/.test(n))return e.textContent="Please enter a 6-digit code.",void(e.style.display="block");e.style.display="none",t.disabled=!0;try{var a=await fetch("/v1/recover/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:recoverEmail,code:n})}),o=await a.json();if(!a.ok)return e.textContent=o.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);if(o.apiKey){document.getElementById("recoveredKeyText").textContent=o.apiKey,showRecoverState("recoverResult");var i=document.querySelector("#recoverResult h2");i&&(i.setAttribute("tabindex","-1"),i.focus())}else e.textContent=o.message||"No key found for this email.",e.style.display="block",t.disabled=!1}catch(n){e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}function copyRecoveredKey(){doCopy(document.getElementById("recoveredKeyText").textContent,document.getElementById("copyRecoveredBtn"))}function doCopy(e,t){function n(){t.textContent="✓ Copied!",setTimeout(function(){t.textContent="Copy"},2e3)}function a(){t.textContent="Failed",setTimeout(function(){t.textContent="Copy"},2e3)}try{navigator.clipboard&&window.isSecureContext?navigator.clipboard.writeText(e).then(n).catch(function(){fallbackCopy(e,n,a)}):fallbackCopy(e,n,a)}catch(e){a()}}function fallbackCopy(e,t,n){try{var a=document.createElement("textarea");a.value=e,a.style.position="fixed",a.style.opacity="0",a.style.top="-9999px",document.body.appendChild(a),a.focus(),a.select();var o=document.execCommand("copy");document.body.removeChild(a),o?t():n()}catch(e){n()}}async function checkout(){try{var e=await fetch("/v1/billing/checkout",{method:"POST"}),t=await e.json();t.url?window.location.href=t.url:alert("Checkout is not available yet. Please try again later.")}catch(e){alert("Something went wrong. Please try again.")}}var pgTemplates={invoice:'\n\n\n\n\n| Description | Qty | Rate | Amount |
|---|---|---|---|
| Web Development — Landing Page | 40 hrs | $150 | $6,000 |
| UI/UX Design — Mockups | 16 hrs | $125 | $2,000 |
| API Integration & Testing | 24 hrs | $150 | $3,600 |
| Total | $11,600 | ||
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.
\nCrossed 10,000 active users milestone in December, two months ahead of target. Enterprise segment grew 200% QoQ.
\nLaunched 3 major features: batch processing, webhook notifications, and custom templates. Template engine adoption reached 40% of Pro users within the first month.
\nQ1 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\nEdit this HTML and watch the preview update in real time.
\nThen click Generate PDF to download it.
\n\n"},previewDebounce=null;function updatePreview(){var e=document.getElementById("demoPreview"),t=document.getElementById("demoHtml").value;if(e){var n=e.contentDocument||e.contentWindow.document;n.open(),n.write(t),n.close()}}function setTemplate(e){document.getElementById("demoHtml").value=pgTemplates[e]||pgTemplates.custom,updatePreview(),document.querySelectorAll(".pg-tab").forEach(function(t){var n=t.getAttribute("data-template")===e;t.classList.toggle("active",n),t.setAttribute("aria-selected",n?"true":"false")})}async function generateDemo(){var e=document.getElementById("demoGenerateBtn"),t=document.getElementById("demoStatus"),n=document.getElementById("demoResult"),a=document.getElementById("demoError"),o=document.getElementById("demoHtml").value.trim();if(!o)return a.textContent="Please enter some HTML.",a.style.display="block",void n.classList.remove("visible");a.style.display="none",n.classList.remove("visible"),e.disabled=!0,e.classList.add("pg-generating"),t.textContent="Generating…";var i=performance.now();try{var r=await fetch("/v1/demo/html",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({html:o})});if(!r.ok){var l=await r.json();return a.textContent=l.error||"Something went wrong.",a.style.display="block",e.disabled=!1,e.classList.remove("pg-generating"),void(t.textContent="")}var d=((performance.now()-i)/1e3).toFixed(1),c=await r.blob(),s=URL.createObjectURL(c);document.getElementById("demoDownload").href=s,document.getElementById("demoTime").textContent=d,n.classList.add("visible"),t.textContent="",e.disabled=!1,e.classList.remove("pg-generating")}catch(n){a.textContent="Network error. Please try again.",a.style.display="block",e.disabled=!1,e.classList.remove("pg-generating"),t.textContent=""}}document.addEventListener("DOMContentLoaded",function(){"#change-email"===window.location.hash&&openEmailChange(),document.getElementById("demoGenerateBtn").addEventListener("click",generateDemo),document.querySelectorAll(".pg-tab").forEach(function(e){e.addEventListener("click",function(){setTemplate(this.getAttribute("data-template"))})}),setTemplate("invoice"),document.getElementById("demoHtml").addEventListener("input",function(){clearTimeout(previewDebounce),previewDebounce=setTimeout(updatePreview,150)});var e=document.getElementById("btn-checkout-playground");e&&e.addEventListener("click",checkout),document.getElementById("btn-checkout").addEventListener("click",checkout);var t=document.getElementById("btn-checkout-hero");t&&t.addEventListener("click",checkout),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){e.target===this&&closeRecover()}),document.querySelectorAll(".open-recover").forEach(function(e){e.addEventListener("click",function(e){e.preventDefault(),openRecover()})}),document.querySelectorAll('a[href^="#"]').forEach(function(e){"demoDownload"!==e.id&&e.addEventListener("click",function(e){var t=this.getAttribute("href");if("#"!==t){e.preventDefault();var n=document.querySelector(t);n&&n.scrollIntoView({behavior:"smooth"})}})})});var emailChangeApiKey="",emailChangeNewEmail="";function showEmailChangeState(e){["emailChangeInitial","emailChangeLoading","emailChangeVerify","emailChangeResult"].forEach(function(e){var t=document.getElementById(e);t&&t.classList.remove("active")}),document.getElementById(e).classList.add("active")}function openEmailChange(){closeRecover(),document.getElementById("emailChangeModal").classList.add("active"),showEmailChangeState("emailChangeInitial");var e=document.getElementById("emailChangeError");e&&(e.style.display="none");var t=document.getElementById("emailChangeVerifyError");t&&(t.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 e=document.getElementById("emailChangeError"),t=document.getElementById("emailChangeBtn"),n=document.getElementById("emailChangeApiKey").value.trim(),a=document.getElementById("emailChangeNewEmail").value.trim();if(!n)return e.textContent="Please enter your API key.",void(e.style.display="block");if(!a||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(a))return e.textContent="Please enter a valid email address.",void(e.style.display="block");e.style.display="none",t.disabled=!0,showEmailChangeState("emailChangeLoading");try{var o=await fetch("/v1/email-change",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:n,newEmail:a})}),i=await o.json();if(!o.ok)return showEmailChangeState("emailChangeInitial"),e.textContent=i.error||"Something went wrong.",e.style.display="block",void(t.disabled=!1);emailChangeApiKey=n,emailChangeNewEmail=a,document.getElementById("emailChangeEmailDisplay").textContent=a,showEmailChangeState("emailChangeVerify"),document.getElementById("emailChangeCode").focus(),t.disabled=!1}catch(n){showEmailChangeState("emailChangeInitial"),e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}async function submitEmailChangeVerify(){var e=document.getElementById("emailChangeVerifyError"),t=document.getElementById("emailChangeVerifyBtn"),n=document.getElementById("emailChangeCode").value.trim();if(!n||!/^\d{6}$/.test(n))return e.textContent="Please enter a 6-digit code.",void(e.style.display="block");e.style.display="none",t.disabled=!0;try{var a=await fetch("/v1/email-change/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:emailChangeApiKey,newEmail:emailChangeNewEmail,code:n})}),o=await a.json();if(!a.ok)return e.textContent=o.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);document.getElementById("emailChangeNewDisplay").textContent=o.newEmail||emailChangeNewEmail,showEmailChangeState("emailChangeResult");var i=document.querySelector("#emailChangeResult h2");i&&(i.setAttribute("tabindex","-1"),i.focus())}catch(n){e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}document.addEventListener("DOMContentLoaded",function(){var e=document.getElementById("btn-close-email-change");e&&e.addEventListener("click",closeEmailChange);var t=document.getElementById("emailChangeBtn");t&&t.addEventListener("click",submitEmailChange);var n=document.getElementById("emailChangeVerifyBtn");n&&n.addEventListener("click",submitEmailChangeVerify);var a=document.getElementById("emailChangeModal");a&&a.addEventListener("click",function(e){e.target===this&&closeEmailChange()}),document.querySelectorAll(".open-email-change").forEach(function(e){e.addEventListener("click",function(e){e.preventDefault(),openEmailChange()})})}),function(){function e(){for(var e=["recoverModal","emailChangeModal"],t=0;t