Files

400 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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:6617439600?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)';
}
if (document.documentElement.lang === 'es') {
timeText = timeText.replace(/Same Day/gi, 'Mismo Día')
.replace(/Same-Day/gi, 'Mismo Día')
.replace(/Hours/gi, 'Horas')
.replace(/Days/gi, 'Días')
.replace(/Routine/gi, 'Rutina')
.replace(/\(Full Crew\)/gi, '(Equipo Completo)');
}
timeDisplay.textContent = timeText;
}
}
// Listen for language changes to update localized calculator output
window.addEventListener('languageChanged', () => {
updateEstimate();
});
// 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'
});
}
});
});
// ==========================================================================
// 7. PODCAST SAMPLE AUDIO CONTROLS (Zero Image / Pure SVG)
// ==========================================================================
const playBtn = document.getElementById('podcast-play-sample');
if (playBtn) {
let isPlaying = false;
let playInterval = null;
let currentSeconds = 0;
const playerTrackFill = document.querySelector('.player-fill');
const playerTime = document.querySelector('.player-time');
playBtn.addEventListener('click', () => {
isPlaying = !isPlaying;
if (isPlaying) {
playBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
playBtn.setAttribute('aria-label', 'Pause sample episode audio');
playInterval = setInterval(() => {
currentSeconds += 1;
if (currentSeconds > 1458) currentSeconds = 0;
const mins = Math.floor(currentSeconds / 60);
const secs = (currentSeconds % 60).toString().padStart(2, '0');
if (playerTime) playerTime.textContent = `${mins}:${secs} / 24:18`;
if (playerTrackFill) {
const pct = Math.min(100, Math.max(0, (currentSeconds / 1458) * 100));
playerTrackFill.style.width = `${pct}%`;
}
}, 1000);
} else {
clearInterval(playInterval);
playBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
playBtn.setAttribute('aria-label', 'Play sample episode audio');
}
});
}
});