feat: initial release for AV Yard Pro (avyardpro.com) AI Pilots client with location pages and Cloudflare Pages setup
This commit is contained in:
@@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user