diff --git a/package.json b/package.json index 0cfbbaa..c806717 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docfast-api", - "version": "0.3.4", + "version": "0.4.0", "description": "Markdown/HTML to PDF API with built-in invoice templates", "main": "dist/index.js", "scripts": { diff --git a/public/app.js b/public/app.js index 3a865bc..aa1806e 100644 --- a/public/app.js +++ b/public/app.js @@ -1,14 +1,5 @@ -var signupEmail = ''; var recoverEmail = ''; -function showState(state) { - ['signupInitial', 'signupLoading', 'signupVerify', 'signupResult'].forEach(function(id) { - var el = document.getElementById(id); - if (el) el.classList.remove('active'); - }); - document.getElementById(state).classList.add('active'); -} - function showRecoverState(state) { ['recoverInitial', 'recoverLoading', 'recoverVerify', 'recoverResult'].forEach(function(id) { var el = document.getElementById(id); @@ -17,23 +8,7 @@ function showRecoverState(state) { document.getElementById(state).classList.add('active'); } -function openSignup() { - document.getElementById('signupModal').classList.add('active'); - showState('signupInitial'); - document.getElementById('signupError').style.display = 'none'; - document.getElementById('verifyError').style.display = 'none'; - document.getElementById('signupEmail').value = ''; - document.getElementById('verifyCode').value = ''; - signupEmail = ''; - setTimeout(function() { document.getElementById('signupEmail').focus(); }, 100); -} - -function closeSignup() { - document.getElementById('signupModal').classList.remove('active'); -} - function openRecover() { - closeSignup(); document.getElementById('recoverModal').classList.add('active'); showRecoverState('recoverInitial'); var errEl = document.getElementById('recoverError'); @@ -50,92 +25,6 @@ function closeRecover() { document.getElementById('recoverModal').classList.remove('active'); } -async function submitSignup() { - var errEl = document.getElementById('signupError'); - var btn = document.getElementById('signupBtn'); - var emailInput = document.getElementById('signupEmail'); - 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; - showState('signupLoading'); - - try { - var res = await fetch('/v1/signup/free', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: email }) - }); - var data = await res.json(); - - if (!res.ok) { - showState('signupInitial'); - errEl.textContent = data.error || 'Something went wrong. Please try again.'; - errEl.style.display = 'block'; - btn.disabled = false; - return; - } - - signupEmail = email; - document.getElementById('verifyEmailDisplay').textContent = email; - showState('signupVerify'); - document.getElementById('verifyCode').focus(); - btn.disabled = false; - } catch (err) { - showState('signupInitial'); - errEl.textContent = 'Network error. Please try again.'; - errEl.style.display = 'block'; - btn.disabled = false; - } -} - -async function submitVerify() { - var errEl = document.getElementById('verifyError'); - var btn = document.getElementById('verifyBtn'); - var codeInput = document.getElementById('verifyCode'); - 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/signup/verify', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: signupEmail, 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('apiKeyText').textContent = data.apiKey; - showState('signupResult'); - var resultH2 = document.querySelector('#signupResult h2'); - if (resultH2) { resultH2.setAttribute('tabindex', '-1'); resultH2.focus(); } - } catch (err) { - errEl.textContent = 'Network error. Please try again.'; - errEl.style.display = 'block'; - btn.disabled = false; - } -} - async function submitRecover() { var errEl = document.getElementById('recoverError'); var btn = document.getElementById('recoverBtn'); @@ -228,12 +117,6 @@ async function submitRecoverVerify() { } } -function copyKey() { - var key = document.getElementById('apiKeyText').textContent; - var btn = document.getElementById('copyBtn'); - doCopy(key, btn); -} - function copyRecoveredKey() { var key = document.getElementById('recoveredKeyText').textContent; var btn = document.getElementById('copyRecoveredBtn'); @@ -252,52 +135,32 @@ function doCopy(text, btn) { try { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(showCopied).catch(function() { - // Fallback to execCommand - try { - var ta = document.createElement('textarea'); - ta.value = text; - ta.style.position = 'fixed'; - ta.style.opacity = '0'; - ta.style.top = '-9999px'; - ta.style.left = '-9999px'; - document.body.appendChild(ta); - ta.focus(); - ta.select(); - var success = document.execCommand('copy'); - document.body.removeChild(ta); - if (success) { - showCopied(); - } else { - showFailed(); - } - } catch (err) { - showFailed(); - } + fallbackCopy(text, showCopied, showFailed); }); } else { - // Direct fallback for non-secure contexts - var ta = document.createElement('textarea'); - ta.value = text; - ta.style.position = 'fixed'; - ta.style.opacity = '0'; - ta.style.top = '-9999px'; - ta.style.left = '-9999px'; - document.body.appendChild(ta); - ta.focus(); - ta.select(); - var success = document.execCommand('copy'); - document.body.removeChild(ta); - if (success) { - showCopied(); - } else { - showFailed(); - } + 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' }); @@ -309,19 +172,71 @@ async function checkout() { } } +// === Demo Playground === +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.style.display = 'none'; + return; + } + + errorEl.style.display = 'none'; + result.style.display = 'none'; + btn.disabled = true; + status.textContent = 'Generating PDF…'; + + 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; + status.textContent = ''; + return; + } + + var blob = await res.blob(); + var url = URL.createObjectURL(blob); + var dl = document.getElementById('demoDownload'); + dl.href = url; + result.style.display = 'block'; + status.textContent = ''; + btn.disabled = false; + } catch (err) { + errorEl.textContent = 'Network error. Please try again.'; + errorEl.style.display = 'block'; + btn.disabled = false; + status.textContent = ''; + } +} + +// === Init === document.addEventListener('DOMContentLoaded', function() { - // BUG-068: Open change email modal if navigated via #change-email hash + // BUG-068: Open change email modal if navigated via hash if (window.location.hash === '#change-email') { openEmailChange(); } - document.getElementById('btn-signup').addEventListener('click', openSignup); - document.getElementById('btn-signup-2').addEventListener('click', openSignup); + // Demo playground + document.getElementById('demoGenerateBtn').addEventListener('click', generateDemo); + + // Checkout buttons document.getElementById('btn-checkout').addEventListener('click', checkout); - document.getElementById('btn-close-signup').addEventListener('click', closeSignup); - document.getElementById('signupBtn').addEventListener('click', submitSignup); - document.getElementById('verifyBtn').addEventListener('click', submitVerify); - document.getElementById('copyBtn').addEventListener('click', copyKey); + var heroCheckout = document.getElementById('btn-checkout-hero'); + if (heroCheckout) heroCheckout.addEventListener('click', checkout); // Recovery modal document.getElementById('btn-close-recover').addEventListener('click', closeRecover); @@ -337,13 +252,13 @@ document.addEventListener('DOMContentLoaded', function() { el.addEventListener('click', function(e) { e.preventDefault(); openRecover(); }); }); - document.getElementById('signupModal').addEventListener('click', function(e) { - if (e.target === this) closeSignup(); - }); + // 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(this.getAttribute('href')); + var el = document.querySelector(target); if (el) el.scrollIntoView({ behavior: 'smooth' }); }); }); @@ -362,7 +277,6 @@ function showEmailChangeState(state) { } function openEmailChange() { - closeSignup(); closeRecover(); document.getElementById('emailChangeModal').classList.add('active'); showEmailChangeState('emailChangeInitial'); @@ -472,7 +386,7 @@ async function submitEmailChangeVerify() { } } -// Add event listeners for email change (append to DOMContentLoaded) +// Email change event listeners document.addEventListener('DOMContentLoaded', function() { var closeBtn = document.getElementById('btn-close-email-change'); if (closeBtn) closeBtn.addEventListener('click', closeEmailChange); @@ -494,7 +408,7 @@ document.addEventListener('DOMContentLoaded', function() { // === Accessibility: Escape key closes modals, focus trapping === (function() { function getActiveModal() { - var modals = ['signupModal', 'recoverModal', 'emailChangeModal']; + 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; @@ -511,7 +425,6 @@ document.addEventListener('DOMContentLoaded', function() { document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeActiveModal(); - // Focus trap inside active modal if (e.key === 'Tab') { var modal = getActiveModal(); if (!modal) return; diff --git a/public/app.min.js b/public/app.min.js index 6b5d8f6..904a38d 100644 --- a/public/app.min.js +++ b/public/app.min.js @@ -1 +1 @@ -var signupEmail="",recoverEmail="";function showState(e){["signupInitial","signupLoading","signupVerify","signupResult"].forEach(function(e){var t=document.getElementById(e);t&&t.classList.remove("active")}),document.getElementById(e).classList.add("active")}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 openSignup(){document.getElementById("signupModal").classList.add("active"),showState("signupInitial"),document.getElementById("signupError").style.display="none",document.getElementById("verifyError").style.display="none",document.getElementById("signupEmail").value="",document.getElementById("verifyCode").value="",signupEmail="",setTimeout(function(){document.getElementById("signupEmail").focus()},100)}function closeSignup(){document.getElementById("signupModal").classList.remove("active")}function openRecover(){closeSignup(),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 submitSignup(){var e=document.getElementById("signupError"),t=document.getElementById("signupBtn"),n=document.getElementById("signupEmail").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,showState("signupLoading");try{var a=await fetch("/v1/signup/free",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:n})}),i=await a.json();if(!a.ok)return showState("signupInitial"),e.textContent=i.error||"Something went wrong. Please try again.",e.style.display="block",void(t.disabled=!1);signupEmail=n,document.getElementById("verifyEmailDisplay").textContent=n,showState("signupVerify"),document.getElementById("verifyCode").focus(),t.disabled=!1}catch(n){showState("signupInitial"),e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}async function submitVerify(){var e=document.getElementById("verifyError"),t=document.getElementById("verifyBtn"),n=document.getElementById("verifyCode").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/signup/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:signupEmail,code:n})}),i=await a.json();if(!a.ok)return e.textContent=i.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);document.getElementById("apiKeyText").textContent=i.apiKey,showState("signupResult"),function(){var h=document.querySelector("#signupResult h2");h&&(h.setAttribute("tabindex","-1"),h.focus())}()}catch(n){e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}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})}),i=await a.json();if(!a.ok)return showRecoverState("recoverInitial"),e.textContent=i.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})}),i=await a.json();if(!a.ok)return e.textContent=i.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);i.apiKey?(document.getElementById("recoveredKeyText").textContent=i.apiKey,showRecoverState("recoverResult"),function(){var h=document.querySelector("#recoverResult h2");h&&(h.setAttribute("tabindex","-1"),h.focus())}()):(e.textContent=i.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 copyKey(){doCopy(document.getElementById("apiKeyText").textContent,document.getElementById("copyBtn"))}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{if(navigator.clipboard&&window.isSecureContext)navigator.clipboard.writeText(e).then(n).catch(function(){try{var t=document.createElement("textarea");t.value=e,t.style.position="fixed",t.style.opacity="0",t.style.top="-9999px",t.style.left="-9999px",document.body.appendChild(t),t.focus(),t.select();var i=document.execCommand("copy");document.body.removeChild(t),i?n():a()}catch(e){a()}});else{var i=document.createElement("textarea");i.value=e,i.style.position="fixed",i.style.opacity="0",i.style.top="-9999px",i.style.left="-9999px",document.body.appendChild(i),i.focus(),i.select();var o=document.execCommand("copy");document.body.removeChild(i),o?n():a()}}catch(e){a()}}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.")}}document.addEventListener("DOMContentLoaded",function(){document.getElementById("btn-signup").addEventListener("click",openSignup),document.getElementById("btn-signup-2").addEventListener("click",openSignup),document.getElementById("btn-checkout").addEventListener("click",checkout),document.getElementById("btn-close-signup").addEventListener("click",closeSignup),document.getElementById("signupBtn").addEventListener("click",submitSignup),document.getElementById("verifyBtn").addEventListener("click",submitVerify),document.getElementById("copyBtn").addEventListener("click",copyKey),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.getElementById("signupModal").addEventListener("click",function(e){e.target===this&&closeSignup()}),document.querySelectorAll('a[href^="#"]').forEach(function(e){e.addEventListener("click",function(e){e.preventDefault();var t=document.querySelector(this.getAttribute("href"));t&&t.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(){closeSignup(),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 i=await fetch("/v1/email-change",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:n,newEmail:a})}),o=await i.json();if(!i.ok)return showEmailChangeState("emailChangeInitial"),e.textContent=o.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})}),i=await a.json();if(!a.ok)return e.textContent=i.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);document.getElementById("emailChangeNewDisplay").textContent=i.newEmail||emailChangeNewEmail,showEmailChangeState("emailChangeResult"),function(){var h=document.querySelector("#emailChangeResult h2");h&&(h.setAttribute("tabindex","-1"),h.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=["signupModal","recoverModal","emailChangeModal"],t=0;t