diff --git a/app.js b/app.js index f9e7522..9c73f53 100644 --- a/app.js +++ b/app.js @@ -52,6 +52,16 @@ close(); }); + const calcBtnText = submitBtn?.querySelector('.calc-btn-text'); + const calcBtnLoading = submitBtn?.querySelector('.calc-btn-loading'); + + function setCalcLoading(on) { + if (!submitBtn) return; + submitBtn.disabled = on; + calcBtnText?.classList.toggle('hidden', on); + calcBtnLoading?.classList.toggle('hidden', !on); + } + submitBtn?.addEventListener('click', async () => { const homesites = parseFloat(document.getElementById('calcHomesites').value) || 0; const propertyType = document.getElementById('calcPropertyType').value; @@ -59,12 +69,15 @@ const paymentFreq = document.getElementById('calcPaymentFreq').value; const reserveFunds = parseFloat(document.getElementById('calcReserveFunds').value) || 0; const interest2025 = parseFloat(document.getElementById('calcInterest2025').value) || 0; + const calcEmail = document.getElementById('calcEmail')?.value.trim() || ''; + const calcOptIn = document.getElementById('calcOptIn')?.checked ?? true; if (!homesites || !annualIncome) { calcErr.classList.remove('hidden'); return; } calcErr.classList.add('hidden'); + setCalcLoading(true); // ── Conservative investment assumptions ── // Operating cash: depending on payment frequency, portion investable in high-yield savings @@ -121,7 +134,11 @@ const aiRes = await fetch('/api/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 }), + body: JSON.stringify({ + homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025, + email: calcEmail, optIn: calcOptIn, + totalPotential, opInterest, resInterest, + }), }); if (aiRes.ok) { const { recommendation } = await aiRes.json(); @@ -136,6 +153,7 @@ // ── Animate the main number ── animateValue(document.getElementById('resultAmount'), 0, totalPotential); + setCalcLoading(false); calcForm.classList.add('hidden'); calcRes.classList.remove('hidden'); }); diff --git a/index.html b/index.html index f71d9b6..7244c26 100644 --- a/index.html +++ b/index.html @@ -489,7 +489,24 @@ - + +
+
+ + +
+ +

🔒 Your email and submitted data are used solely to provide your ROI estimate and will never be shared with any third party or outside organization.

+
+ + +

Conservative estimates for illustrative purposes only. Not financial advice.

@@ -520,6 +537,11 @@

+
+ +

This is a high-level estimate based on your inputs. Inside HOA LedgerIQ, your investment strategy is continuously refined against your live cash flows, upcoming expenses, and reserve study timelines — automatically maximizing yield while keeping your community fully liquid.

+
+

Ready to put these gains to work for your community?

Get Early Access — Reserve My Spot → diff --git a/server.js b/server.js index b86105f..db2f759 100644 --- a/server.js +++ b/server.js @@ -53,6 +53,25 @@ db.exec(` ); `); +db.exec(` + CREATE TABLE IF NOT EXISTS calc_submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT, + opt_in INTEGER DEFAULT 1, + homesites REAL, + property_type TEXT, + annual_income REAL, + payment_freq TEXT, + reserve_funds REAL, + interest_2025 REAL, + total_potential REAL, + op_interest REAL, + res_interest REAL, + ai_recommendation TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) + ); +`); + // Migrate existing DBs: add new columns if they don't exist yet const cols = db.pragma('table_info(leads)').map(c => c.name); if (!cols.includes('org_name')) db.exec('ALTER TABLE leads ADD COLUMN org_name TEXT'); @@ -67,6 +86,19 @@ const insertLead = db.prepare(` const findByEmail = db.prepare(`SELECT id FROM leads WHERE email = ? LIMIT 1`); +const insertCalcSubmission = db.prepare(` + INSERT INTO calc_submissions + (email, opt_in, homesites, property_type, annual_income, payment_freq, + reserve_funds, interest_2025, total_potential, op_interest, res_interest, ai_recommendation) + VALUES + (@email, @optIn, @homesites, @propertyType, @annualIncome, @paymentFreq, + @reserveFunds, @interest2025, @totalPotential, @opInterest, @resInterest, @aiRecommendation) +`); + +const getAllCalcSubmissions = db.prepare(` + SELECT * FROM calc_submissions ORDER BY created_at DESC +`); + const getAllLeads = db.prepare(` SELECT id, first_name, last_name, email, org_name, state, role, unit_count, beta_interest, source, created_at FROM leads @@ -140,11 +172,36 @@ app.get('/api/leads', (req, res) => { // POST /api/calculate — AI-powered investment recommendation app.post('/api/calculate', async (req, res) => { + function saveCalcSubmission(aiRecommendation) { + try { + insertCalcSubmission.run({ + email: email?.trim() || null, + optIn: optIn ? 1 : 0, + homesites: homesites || null, + propertyType: propertyType || null, + annualIncome: annualIncome || null, + paymentFreq: paymentFreq || null, + reserveFunds: reserveFunds || null, + interest2025: interest2025 || null, + totalPotential: totalPotential || null, + opInterest: opInterest || null, + resInterest: resInterest || null, + aiRecommendation: aiRecommendation || null, + }); + } catch (err) { + console.error('Failed to save calc submission:', err.message); + } + } + if (!aiClient) { + saveCalcSubmission(null); return res.status(503).json({ error: 'AI service not configured.' }); } - const { homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 } = req.body ?? {}; + const { + homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025, + email, optIn, totalPotential, opInterest, resInterest, + } = req.body ?? {}; if (!homesites || !annualIncome) { return res.status(400).json({ error: 'homesites and annualIncome are required.' }); @@ -185,13 +242,25 @@ Keep the tone professional and factual. No bullet points — flowing paragraph o if (AI_DEBUG) console.log('[AI_DEBUG] response:', text); + saveCalcSubmission(text); res.json({ recommendation: text }); } catch (err) { console.error('AI API error:', err.message); + saveCalcSubmission(null); res.status(502).json({ error: 'AI service unavailable. Showing estimated result.' }); } }); +// GET /api/calc-submissions — internal: list all calculator submissions +app.get('/api/calc-submissions', (req, res) => { + const secret = req.headers['x-admin-key']; + if (!secret || secret !== process.env.ADMIN_KEY) { + return res.status(401).json({ error: 'Unauthorized.' }); + } + const rows = getAllCalcSubmissions.all(); + res.json({ count: rows.length, submissions: rows }); +}); + // Health check app.get('/api/health', (_req, res) => res.json({ status: 'ok', ts: new Date().toISOString() })); diff --git a/styles.css b/styles.css index a243222..5f695cf 100644 --- a/styles.css +++ b/styles.css @@ -840,6 +840,50 @@ body { .calc-recalc { font-size: 14px; color: var(--gray-500); } .calc-recalc:hover { color: var(--gray-200); } +/* ── Calc email + opt-in ── */ +.calc-email-row { margin-top: 20px; display: flex; flex-direction: column; gap: 10px; } +.calc-field--full { grid-column: 1 / -1; } +.calc-optional { font-weight: 400; color: var(--gray-600); font-size: 11px; } +.calc-optin-label { + display: flex; align-items: flex-start; gap: 10px; + font-size: 13px; color: var(--gray-300); cursor: pointer; line-height: 1.5; +} +.calc-optin-label input[type="checkbox"] { + margin-top: 2px; accent-color: var(--blue); flex-shrink: 0; + width: 15px; height: 15px; cursor: pointer; +} +.calc-privacy { + font-size: 11px; color: var(--gray-600); line-height: 1.5; + border-left: 2px solid rgba(255,255,255,0.06); padding-left: 10px; margin: 0; +} + +/* ── Calc button spinner ── */ +.calc-btn-loading { display: flex; align-items: center; gap: 8px; } +.calc-spinner { + display: inline-block; width: 14px; height: 14px; + border: 2px solid rgba(255,255,255,0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* ── Results refinement blurb ── */ +.calc-refine-blurb { + display: flex; gap: 12px; align-items: flex-start; + background: rgba(14,165,233,0.07); + border: 1px solid rgba(14,165,233,0.18); + border-radius: 10px; + padding: 14px 16px; + margin-bottom: 20px; +} +.calc-refine-icon { + font-size: 18px; color: var(--sky); flex-shrink: 0; margin-top: 1px; +} +.calc-refine-blurb p { + font-size: 12.5px; color: var(--gray-400); line-height: 1.6; margin: 0; +} + /* ---- Screenshot Carousel ---- */ .screenshot-carousel { width: 100%;