350 lines
12 KiB
JavaScript
350 lines
12 KiB
JavaScript
/**
|
||
* 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'
|
||
});
|
||
}
|
||
});
|
||
});
|
||
});
|