AI text is still generated and saved to calc_submissions for internal follow-up, but no longer displayed to the user — keeps the detailed analysis behind the product sign-up wall. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
324 lines
12 KiB
JavaScript
324 lines
12 KiB
JavaScript
/* HOA LedgerIQ — Frontend form handler + countdown */
|
|
|
|
// ── Dynamic Countdown ────────────────────────────────────
|
|
// Launch date = March 1, 2026 + 60 days = April 30, 2026
|
|
// On 3/1/2026 the banner shows "60 days", then counts down daily.
|
|
(function initCountdown() {
|
|
const launchDate = new Date('2026-04-30T00:00:00');
|
|
const now = new Date();
|
|
const msPerDay = 1000 * 60 * 60 * 24;
|
|
const daysLeft = Math.max(0, Math.ceil((launchDate - now) / msPerDay));
|
|
|
|
const label = daysLeft === 1 ? 'Day' : 'Days';
|
|
const text = daysLeft > 0
|
|
? `Launching in ${daysLeft} ${label}`
|
|
: 'Now Live!';
|
|
|
|
const bannerEl = document.getElementById('bannerCountdown');
|
|
const signupEl = document.getElementById('signupCountdown');
|
|
if (bannerEl) bannerEl.textContent = text;
|
|
if (signupEl) signupEl.textContent = text;
|
|
})();
|
|
|
|
// ── Benefit Calculator ───────────────────────────────────
|
|
(function initCalculator() {
|
|
const overlay = document.getElementById('calcOverlay');
|
|
const openBtn = document.getElementById('openCalc');
|
|
const closeBtn = document.getElementById('calcClose');
|
|
const submitBtn = document.getElementById('calcSubmit');
|
|
const recalcBtn = document.getElementById('calcRecalc');
|
|
const calcForm = document.getElementById('calcForm');
|
|
const calcRes = document.getElementById('calcResults');
|
|
const calcErr = document.getElementById('calcError');
|
|
const ctaBtn = document.getElementById('calcCTABtn');
|
|
|
|
if (!overlay) return;
|
|
|
|
function open() { overlay.classList.add('open'); document.body.style.overflow = 'hidden'; }
|
|
function close() { overlay.classList.remove('open'); document.body.style.overflow = ''; }
|
|
|
|
openBtn?.addEventListener('click', open);
|
|
closeBtn?.addEventListener('click', close);
|
|
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
|
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
|
|
|
|
recalcBtn?.addEventListener('click', () => {
|
|
calcRes.classList.add('hidden');
|
|
calcForm.classList.remove('hidden');
|
|
});
|
|
|
|
// Close modal and scroll to signup when CTA clicked
|
|
ctaBtn?.addEventListener('click', () => {
|
|
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;
|
|
const annualIncome = parseFloat(document.getElementById('calcAnnualIncome').value) || 0;
|
|
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
|
|
const opMultiplier = { monthly: 0.10, quarterly: 0.20, annually: 0.35 }[paymentFreq] || 0.10;
|
|
const opRate = 0.040; // 4.0% money market / HYSA
|
|
const resRatio = 0.65; // 65% of reserves investable (keep 35% liquid)
|
|
const resRate = 0.0425; // 4.25% CD ladder / short-term treasuries
|
|
|
|
const investableOp = annualIncome * opMultiplier;
|
|
const investableRes = reserveFunds * resRatio;
|
|
const opInterest = Math.round(investableOp * opRate);
|
|
const resInterest = Math.round(investableRes * resRate);
|
|
const totalPotential = opInterest + resInterest;
|
|
const increase = totalPotential - interest2025;
|
|
const pctIncrease = interest2025 > 0
|
|
? Math.round((increase / interest2025) * 100)
|
|
: (totalPotential > 0 ? 100 : 0);
|
|
|
|
// ── Populate results ──
|
|
const fmt = n => '$' + Math.round(n).toLocaleString();
|
|
|
|
document.getElementById('resultAmount').textContent = fmt(totalPotential);
|
|
document.getElementById('resultCurrent').textContent = fmt(interest2025);
|
|
document.getElementById('resultOperating').textContent = fmt(opInterest);
|
|
document.getElementById('resultReserve').textContent = fmt(resInterest);
|
|
|
|
const badge = document.getElementById('resultBadge');
|
|
if (increase > 0) {
|
|
badge.textContent = `+${fmt(increase)} · +${pctIncrease}%`;
|
|
badge.style.display = 'inline-block';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
|
|
// ── AI-style suggestion text ──
|
|
const typeLabels = { sfh:'single-family home', townhomes:'townhome', condos:'condo', mixed:'mixed-use', '':'' };
|
|
const typeLabel = typeLabels[propertyType] || '';
|
|
const freqLabel = { monthly:'monthly', quarterly:'quarterly', annually:'annual' }[paymentFreq];
|
|
const communityDesc = [homesites && `${homesites}-unit`, typeLabel, 'community'].filter(Boolean).join(' ');
|
|
|
|
let ai = `Based on your ${communityDesc} collecting ${fmt(annualIncome)} in ${freqLabel} dues`;
|
|
if (reserveFunds > 0) ai += ` and ${fmt(reserveFunds)} in reserve funds`;
|
|
ai += `, a conservative investment strategy could generate approximately ${fmt(totalPotential)} in annual interest income. `;
|
|
if (resInterest > 0) ai += `Deploying ${fmt(investableRes)} of your reserve funds into a short-term CD ladder at ~4.25% yields ${fmt(resInterest)} annually. `;
|
|
if (opInterest > 0) ai += `Keeping a ${fmt(investableOp)} operating cash buffer in a high-yield money market at ~4.0% adds another ${fmt(opInterest)}. `;
|
|
if (interest2025 > 0 && increase > 0) {
|
|
ai += `That's a ${fmt(increase)} improvement (+${pctIncrease}%) over your 2025 interest income of ${fmt(interest2025)} — with no additional risk.`;
|
|
} else if (interest2025 === 0) {
|
|
ai += `This would represent entirely new interest income for your community at no additional risk.`;
|
|
}
|
|
|
|
// ── AI recommendation — call server to generate & save to DB (not displayed) ──
|
|
try {
|
|
await fetch('/api/calculate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025,
|
|
email: calcEmail, optIn: calcOptIn,
|
|
totalPotential, opInterest, resInterest,
|
|
}),
|
|
});
|
|
} catch (_) { /* best-effort — DB save failed silently */ }
|
|
|
|
// ── Animate the main number ──
|
|
animateValue(document.getElementById('resultAmount'), 0, totalPotential);
|
|
|
|
setCalcLoading(false);
|
|
calcForm.classList.add('hidden');
|
|
calcRes.classList.remove('hidden');
|
|
});
|
|
|
|
function animateValue(el, from, to) {
|
|
const duration = 900;
|
|
const start = performance.now();
|
|
function step(now) {
|
|
const progress = Math.min((now - start) / duration, 1);
|
|
const ease = 1 - Math.pow(1 - progress, 3);
|
|
el.textContent = '$' + Math.round(from + (to - from) * ease).toLocaleString();
|
|
if (progress < 1) requestAnimationFrame(step);
|
|
}
|
|
requestAnimationFrame(step);
|
|
}
|
|
})();
|
|
|
|
// ── Screenshot Carousel ──────────────────────────────────
|
|
(function initCarousel() {
|
|
const carousel = document.getElementById('screenshotCarousel');
|
|
if (!carousel) return;
|
|
|
|
const slides = carousel.querySelectorAll('.carousel-slide');
|
|
const dots = carousel.querySelectorAll('.carousel-dot');
|
|
let current = 0;
|
|
let timer;
|
|
|
|
function goTo(index) {
|
|
slides[current].classList.remove('active');
|
|
dots[current].classList.remove('active');
|
|
current = (index + slides.length) % slides.length;
|
|
slides[current].classList.add('active');
|
|
dots[current].classList.add('active');
|
|
}
|
|
|
|
function next() { goTo(current + 1); }
|
|
function prev() { goTo(current - 1); }
|
|
|
|
function startAuto() {
|
|
timer = setInterval(next, 4500);
|
|
}
|
|
function resetAuto() {
|
|
clearInterval(timer);
|
|
startAuto();
|
|
}
|
|
|
|
carousel.querySelector('.carousel-next').addEventListener('click', () => { next(); resetAuto(); });
|
|
carousel.querySelector('.carousel-prev').addEventListener('click', () => { prev(); resetAuto(); });
|
|
|
|
dots.forEach(dot => {
|
|
dot.addEventListener('click', () => {
|
|
goTo(parseInt(dot.dataset.index, 10));
|
|
resetAuto();
|
|
});
|
|
});
|
|
|
|
// Pause on hover
|
|
carousel.addEventListener('mouseenter', () => clearInterval(timer));
|
|
carousel.addEventListener('mouseleave', startAuto);
|
|
|
|
startAuto();
|
|
})();
|
|
|
|
// ── Form Handler ─────────────────────────────────────────
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const form = document.getElementById('signupForm');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const btnText = submitBtn?.querySelector('.btn-text');
|
|
const btnLoading = submitBtn?.querySelector('.btn-loading');
|
|
const successDiv = document.getElementById('signupSuccess');
|
|
|
|
if (!form) return;
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const firstName = form.firstName.value.trim();
|
|
const lastName = form.lastName.value.trim();
|
|
const email = form.email.value.trim();
|
|
|
|
const orgName = form.orgName?.value.trim() || '';
|
|
const state = form.state?.value || '';
|
|
|
|
// Basic client-side validation
|
|
if (!firstName || !lastName || !email || !orgName || !state) {
|
|
highlightEmpty(form);
|
|
return;
|
|
}
|
|
if (!isValidEmail(email)) {
|
|
form.email.style.borderColor = '#ef4444';
|
|
form.email.focus();
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
setLoading(true);
|
|
|
|
const payload = {
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
orgName: form.orgName?.value.trim() || '',
|
|
state: form.state?.value || '',
|
|
role: form.role.value,
|
|
unitCount: form.unitCount.value,
|
|
betaInterest: form.betaInterest?.checked || false,
|
|
source: 'landing_page',
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/leads', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (res.ok) {
|
|
showSuccess();
|
|
} else {
|
|
const data = await res.json().catch(() => ({}));
|
|
if (res.status === 409) {
|
|
// Already registered
|
|
showSuccess('You\'re already on the list! We\'ll be in touch soon.');
|
|
} else {
|
|
alert(data.error || 'Something went wrong. Please try again.');
|
|
setLoading(false);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Network error — fall back to localStorage so we don't lose the lead
|
|
saveLocally(payload);
|
|
showSuccess();
|
|
}
|
|
});
|
|
|
|
function setLoading(on) {
|
|
if (!btnText || !btnLoading) return;
|
|
submitBtn.disabled = on;
|
|
btnText.classList.toggle('hidden', on);
|
|
btnLoading.classList.toggle('hidden', !on);
|
|
}
|
|
|
|
function showSuccess(msg) {
|
|
form.classList.add('hidden');
|
|
if (msg && successDiv) {
|
|
const p = successDiv.querySelector('p');
|
|
if (p) p.textContent = msg;
|
|
}
|
|
successDiv?.classList.remove('hidden');
|
|
}
|
|
|
|
function highlightEmpty(f) {
|
|
['firstName', 'lastName', 'email', 'orgName', 'state'].forEach(name => {
|
|
const el = f[name];
|
|
if (el && !el.value.trim()) {
|
|
el.style.borderColor = '#ef4444';
|
|
el.addEventListener('input', () => { el.style.borderColor = ''; }, { once: true });
|
|
el.addEventListener('change', () => { el.style.borderColor = ''; }, { once: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
function isValidEmail(email) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
}
|
|
|
|
function saveLocally(payload) {
|
|
try {
|
|
const existing = JSON.parse(localStorage.getItem('hoa_leads') || '[]');
|
|
existing.push(payload);
|
|
localStorage.setItem('hoa_leads', JSON.stringify(existing));
|
|
} catch (_) { /* best-effort */ }
|
|
}
|
|
});
|