feat: initial release for AV Yard Pro (avyardpro.com) AI Pilots client with location pages and Cloudflare Pages setup

This commit is contained in:
2026-09-11 19:54:29 -07:00
commit bc9dea03fd
25 changed files with 8203 additions and 0 deletions
+349
View File
@@ -0,0 +1,349 @@
/**
* NEXT GEN LANDSCAPING & YARD CARE
* Core Interactive Logic & Calculator Engine
*/
document.addEventListener('DOMContentLoaded', () => {
// 1. Current Year in Footer
const yearSpan = document.getElementById('current-year');
if (yearSpan) {
yearSpan.textContent = new Date().getFullYear();
}
// 2. Mobile Navigation Toggle
const mobileToggle = document.getElementById('mobile-toggle');
const mobileDrawer = document.getElementById('mobile-drawer');
const mobileLinks = document.querySelectorAll('.mobile-link');
if (mobileToggle && mobileDrawer) {
mobileToggle.addEventListener('click', () => {
mobileDrawer.classList.toggle('open');
});
mobileLinks.forEach(link => {
link.addEventListener('click', () => {
mobileDrawer.classList.remove('open');
});
});
}
// 3. Native <dialog> Modal Management
const quoteDialog = document.getElementById('quote-dialog');
const modalCloseBtn = document.getElementById('modal-close');
const openQuoteButtons = document.querySelectorAll('.open-quote-btn');
const quoteForm = document.getElementById('quote-form');
const serviceSelect = document.getElementById('lead-service');
const notesTextarea = document.getElementById('lead-notes');
const toast = document.getElementById('toast');
// Open Modal function
const openModal = (initialScope = null, initialNotes = null) => {
if (!quoteDialog) return;
if (initialScope && serviceSelect) {
for (let i = 0; i < serviceSelect.options.length; i++) {
if (serviceSelect.options[i].text.toLowerCase().includes(initialScope.toLowerCase()) ||
serviceSelect.options[i].value.toLowerCase().includes(initialScope.toLowerCase())) {
serviceSelect.selectedIndex = i;
break;
}
}
}
if (initialNotes && notesTextarea) {
notesTextarea.value = initialNotes;
}
quoteDialog.showModal();
};
openQuoteButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
const scope = btn.getAttribute('data-scope');
openModal(scope);
});
});
if (modalCloseBtn) {
modalCloseBtn.addEventListener('click', () => {
quoteDialog.close();
});
}
// Click outside to close native dialog
if (quoteDialog) {
quoteDialog.addEventListener('click', (e) => {
const rect = quoteDialog.getBoundingClientRect();
const isInDialog = (
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width
);
if (!isInDialog) {
quoteDialog.close();
}
});
}
// Handle Modal Quote Form Submission
if (quoteForm) {
quoteForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('lead-name')?.value || '';
const phone = document.getElementById('lead-phone')?.value || '';
const address = document.getElementById('lead-address')?.value || '';
const service = document.getElementById('lead-service')?.value || 'Yard Cleanup';
const notes = document.getElementById('lead-notes')?.value || '';
const leadData = {
id: 'NG-' + Date.now().toString(36).toUpperCase(),
source: 'modal_dialog',
name,
phone,
address,
service,
notes,
submittedAt: new Date().toISOString()
};
try {
const existing = JSON.parse(localStorage.getItem('nextgen_leads') || '[]');
existing.unshift(leadData);
localStorage.setItem('nextgen_leads', JSON.stringify(existing));
} catch (err) {
console.error('Storage error:', err);
}
// Close dialog
quoteDialog.close();
// Show Toast Notification
if (toast) {
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 6000);
}
// Reset Form
quoteForm.reset();
});
}
// Handle Dedicated Contact Page Form Submission
const contactForm = document.getElementById('contact-form');
const contactSuccessBox = document.getElementById('contact-success-box');
const contactSmsLink = document.getElementById('contact-sms-link');
const contactLeadIdSpan = document.getElementById('contact-lead-id');
if (contactForm) {
contactForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('contact-name')?.value || '';
const phone = document.getElementById('contact-phone')?.value || '';
const address = document.getElementById('contact-address')?.value || '';
const service = document.getElementById('contact-service')?.value || 'Yard Cleanup';
const urgency = document.getElementById('contact-urgency')?.value || 'Flexible';
const size = document.getElementById('contact-size')?.value || 'Standard';
const notes = document.getElementById('contact-notes')?.value || '';
const leadId = 'NG-' + Date.now().toString(36).toUpperCase();
const leadData = {
id: leadId,
source: 'contact_page',
name,
phone,
address,
service,
urgency,
size,
notes,
submittedAt: new Date().toISOString()
};
try {
const existing = JSON.parse(localStorage.getItem('nextgen_leads') || '[]');
existing.unshift(leadData);
localStorage.setItem('nextgen_leads', JSON.stringify(existing));
} catch (err) {
console.error('Storage error:', err);
}
// Update UI to success state
contactForm.style.display = 'none';
if (contactLeadIdSpan) {
contactLeadIdSpan.textContent = leadId;
}
if (contactSuccessBox) {
contactSuccessBox.classList.add('show');
}
// Build instant mobile SMS follow-up link
if (contactSmsLink) {
const textMessage = `Hi Next Gen! My name is ${name} at ${address}. I submitted quote request #${leadId} for ${service} (${size}). Please let me know your availability!`;
contactSmsLink.href = `sms:6615817278?body=${encodeURIComponent(textMessage)}`;
}
// Trigger Toast
if (toast) {
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 6000);
}
});
}
// ==========================================================================
// 4. INTERACTIVE YARD COST ESTIMATOR LOGIC
// ==========================================================================
const priceDisplay = document.getElementById('calc-price-display');
const timeDisplay = document.getElementById('calc-time-display');
const calcBookBtn = document.getElementById('calc-book-btn');
// Base pricing matrix [Min, Max]
const serviceRates = {
'cleanup': { base: [350, 550], time: 'Same Day (3 - 5 Hours)' },
'fire-abatement': { base: [450, 800], time: 'Same Day (4 - 6 Hours)' },
'turf': { base: [2200, 4800], time: '2 - 3 Days' },
'rockscape': { base: [1800, 3900], time: '2 - 4 Days' },
'trees': { base: [400, 950], time: 'Same Day (3 - 6 Hours)' },
'maintenance': { base: [180, 320], time: 'Routine (1.5 - 2.5 Hours)' }
};
const sizeMultipliers = {
'small': 0.75,
'medium': 1.0,
'large': 1.8,
'acreage': 3.2
};
const conditionMultipliers = {
'mild': 0.85,
'heavy': 1.15,
'severe': 1.55
};
function updateEstimate() {
const selectedService = document.querySelector('input[name="calc-service"]:checked')?.value || 'cleanup';
const selectedSize = document.querySelector('input[name="calc-size"]:checked')?.value || 'medium';
const selectedCondition = document.querySelector('input[name="calc-condition"]:checked')?.value || 'heavy';
const serviceData = serviceRates[selectedService] || serviceRates['cleanup'];
const sizeMult = sizeMultipliers[selectedSize] || 1.0;
const condMult = conditionMultipliers[selectedCondition] || 1.0;
let minPrice = Math.round((serviceData.base[0] * sizeMult * condMult) / 25) * 25;
let maxPrice = Math.round((serviceData.base[1] * sizeMult * condMult) / 25) * 25;
if (priceDisplay) {
priceDisplay.textContent = `$${minPrice.toLocaleString()} $${maxPrice.toLocaleString()}`;
}
if (timeDisplay) {
let timeText = serviceData.time;
if (selectedSize === 'acreage' && !timeText.includes('Days')) {
timeText = '1 - 2 Days (Full Crew)';
}
timeDisplay.textContent = timeText;
}
}
// Listen to all calculator radio buttons
const calcInputs = document.querySelectorAll('input[name="calc-service"], input[name="calc-size"], input[name="calc-condition"]');
calcInputs.forEach(input => {
input.addEventListener('change', updateEstimate);
});
// Calculate Initial Estimate
updateEstimate();
// Book with current estimate scope
if (calcBookBtn) {
calcBookBtn.addEventListener('click', () => {
const selectedServiceEl = document.querySelector('input[name="calc-service"]:checked');
const selectedSizeEl = document.querySelector('input[name="calc-size"]:checked');
const selectedConditionEl = document.querySelector('input[name="calc-condition"]:checked');
const serviceTitle = selectedServiceEl?.closest('.calc-option')?.querySelector('.calc-option-title')?.textContent || 'Yardwork';
const sizeTitle = selectedSizeEl?.closest('.calc-option')?.querySelector('.calc-option-title')?.textContent || 'Medium Lot';
const conditionTitle = selectedConditionEl?.closest('.calc-option')?.querySelector('.calc-option-title')?.textContent || 'Heavy Overgrowth';
const currentPrice = priceDisplay?.textContent || '';
const notes = `Calculator Estimate Scope: ${serviceTitle} | Property: ${sizeTitle} | Condition: ${conditionTitle} | Estimated Range: ${currentPrice}`;
openModal(serviceTitle, notes);
});
}
// ==========================================================================
// 5. INTERACTIVE BEFORE & AFTER SLIDER
// ==========================================================================
const baContainer = document.getElementById('ba-slider');
const baLayer = document.getElementById('ba-layer');
const baHandle = document.getElementById('ba-handle');
if (baContainer && baLayer && baHandle) {
let isDragging = false;
const setSliderPosition = (x) => {
const rect = baContainer.getBoundingClientRect();
let pos = ((x - rect.left) / rect.width) * 100;
pos = Math.max(0, Math.min(100, pos)); // clamp 0 - 100
baContainer.style.setProperty('--slider-pos', `${pos}%`);
};
// Mouse events
baContainer.addEventListener('mousedown', (e) => {
isDragging = true;
setSliderPosition(e.clientX);
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
setSliderPosition(e.clientX);
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
// Touch events for mobile/tablet
baContainer.addEventListener('touchstart', (e) => {
isDragging = true;
if (e.touches.length > 0) {
setSliderPosition(e.touches[0].clientX);
}
}, { passive: true });
window.addEventListener('touchmove', (e) => {
if (!isDragging || e.touches.length === 0) return;
setSliderPosition(e.touches[0].clientX);
}, { passive: true });
window.addEventListener('touchend', () => {
isDragging = false;
});
}
// 6. Smooth Scroll Header Offset
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const href = this.getAttribute('href');
if (href === '#' || !href.startsWith('#')) return;
const target = document.querySelector(href);
if (target) {
e.preventDefault();
const headerHeight = document.querySelector('.site-header')?.offsetHeight || 80;
const targetPos = target.getBoundingClientRect().top + window.pageYOffset - headerHeight;
window.scrollTo({
top: targetPos,
behavior: 'smooth'
});
}
});
});
});