339 lines
10 KiB
JavaScript
339 lines
10 KiB
JavaScript
|
|
// HarborSmith - Main JavaScript
|
||
|
|
// ========================
|
||
|
|
|
||
|
|
// Theme Management
|
||
|
|
const themeManager = {
|
||
|
|
init() {
|
||
|
|
this.themeToggle = document.getElementById('themeToggle');
|
||
|
|
this.themeDropdown = document.getElementById('themeDropdown');
|
||
|
|
this.themeOptions = document.querySelectorAll('.theme-option');
|
||
|
|
|
||
|
|
// Load saved theme or default to nautical
|
||
|
|
const savedTheme = localStorage.getItem('harborsmith-theme') || 'nautical';
|
||
|
|
this.setTheme(savedTheme);
|
||
|
|
|
||
|
|
// Event listeners
|
||
|
|
this.themeToggle?.addEventListener('click', (e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
this.themeDropdown.classList.toggle('active');
|
||
|
|
});
|
||
|
|
|
||
|
|
this.themeOptions.forEach(option => {
|
||
|
|
option.addEventListener('click', (e) => {
|
||
|
|
const theme = e.currentTarget.dataset.theme;
|
||
|
|
this.setTheme(theme);
|
||
|
|
this.themeDropdown.classList.remove('active');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Close dropdown when clicking outside
|
||
|
|
document.addEventListener('click', () => {
|
||
|
|
this.themeDropdown?.classList.remove('active');
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
setTheme(theme) {
|
||
|
|
document.documentElement.setAttribute('data-theme', theme);
|
||
|
|
localStorage.setItem('harborsmith-theme', theme);
|
||
|
|
|
||
|
|
// Update active state
|
||
|
|
this.themeOptions.forEach(option => {
|
||
|
|
option.classList.toggle('active', option.dataset.theme === theme);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Navigation
|
||
|
|
const navigation = {
|
||
|
|
init() {
|
||
|
|
this.navbar = document.getElementById('navbar');
|
||
|
|
this.navToggle = document.getElementById('navToggle');
|
||
|
|
this.navMenu = document.getElementById('navMenu');
|
||
|
|
this.navLinks = document.querySelectorAll('.nav-link');
|
||
|
|
|
||
|
|
// Mobile menu toggle
|
||
|
|
this.navToggle?.addEventListener('click', () => {
|
||
|
|
this.navToggle.classList.toggle('active');
|
||
|
|
this.navMenu.classList.toggle('active');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Close mobile menu on link click
|
||
|
|
this.navLinks.forEach(link => {
|
||
|
|
link.addEventListener('click', () => {
|
||
|
|
this.navToggle?.classList.remove('active');
|
||
|
|
this.navMenu?.classList.remove('active');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Scroll behavior
|
||
|
|
window.addEventListener('scroll', () => {
|
||
|
|
if (window.scrollY > 50) {
|
||
|
|
this.navbar?.classList.add('scrolled');
|
||
|
|
} else {
|
||
|
|
this.navbar?.classList.remove('scrolled');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Active link highlighting
|
||
|
|
this.updateActiveLink();
|
||
|
|
},
|
||
|
|
|
||
|
|
updateActiveLink() {
|
||
|
|
const currentPath = window.location.pathname.split('/').pop() || 'index.html';
|
||
|
|
this.navLinks.forEach(link => {
|
||
|
|
const href = link.getAttribute('href');
|
||
|
|
if (href === currentPath) {
|
||
|
|
link.classList.add('active');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Smooth Scrolling
|
||
|
|
const smoothScroll = {
|
||
|
|
init() {
|
||
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||
|
|
anchor.addEventListener('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const target = document.querySelector(anchor.getAttribute('href'));
|
||
|
|
if (target) {
|
||
|
|
target.scrollIntoView({
|
||
|
|
behavior: 'smooth',
|
||
|
|
block: 'start'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Testimonial Slider
|
||
|
|
const testimonialSlider = {
|
||
|
|
init() {
|
||
|
|
this.cards = document.querySelectorAll('.testimonial-card');
|
||
|
|
this.dots = document.querySelectorAll('.dot');
|
||
|
|
this.currentIndex = 0;
|
||
|
|
|
||
|
|
if (this.cards.length === 0) return;
|
||
|
|
|
||
|
|
// Auto-play
|
||
|
|
this.startAutoPlay();
|
||
|
|
|
||
|
|
// Dot navigation
|
||
|
|
this.dots.forEach((dot, index) => {
|
||
|
|
dot.addEventListener('click', () => {
|
||
|
|
this.goToSlide(index);
|
||
|
|
this.resetAutoPlay();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
goToSlide(index) {
|
||
|
|
this.cards[this.currentIndex]?.classList.remove('active');
|
||
|
|
this.dots[this.currentIndex]?.classList.remove('active');
|
||
|
|
|
||
|
|
this.currentIndex = index;
|
||
|
|
|
||
|
|
this.cards[this.currentIndex]?.classList.add('active');
|
||
|
|
this.dots[this.currentIndex]?.classList.add('active');
|
||
|
|
},
|
||
|
|
|
||
|
|
nextSlide() {
|
||
|
|
const nextIndex = (this.currentIndex + 1) % this.cards.length;
|
||
|
|
this.goToSlide(nextIndex);
|
||
|
|
},
|
||
|
|
|
||
|
|
startAutoPlay() {
|
||
|
|
this.autoPlayInterval = setInterval(() => {
|
||
|
|
this.nextSlide();
|
||
|
|
}, 5000);
|
||
|
|
},
|
||
|
|
|
||
|
|
resetAutoPlay() {
|
||
|
|
clearInterval(this.autoPlayInterval);
|
||
|
|
this.startAutoPlay();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Counter Animation
|
||
|
|
const counterAnimation = {
|
||
|
|
init() {
|
||
|
|
this.counters = document.querySelectorAll('.stat-number');
|
||
|
|
this.animated = false;
|
||
|
|
|
||
|
|
if (this.counters.length === 0) return;
|
||
|
|
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting && !this.animated) {
|
||
|
|
this.animateCounters();
|
||
|
|
this.animated = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, { threshold: 0.5 });
|
||
|
|
|
||
|
|
const statsSection = document.querySelector('.stats-section');
|
||
|
|
if (statsSection) {
|
||
|
|
observer.observe(statsSection);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
animateCounters() {
|
||
|
|
this.counters.forEach(counter => {
|
||
|
|
const target = parseInt(counter.dataset.count);
|
||
|
|
const duration = 2000;
|
||
|
|
const increment = target / (duration / 16);
|
||
|
|
let current = 0;
|
||
|
|
|
||
|
|
const updateCounter = () => {
|
||
|
|
current += increment;
|
||
|
|
if (current < target) {
|
||
|
|
counter.textContent = Math.floor(current);
|
||
|
|
requestAnimationFrame(updateCounter);
|
||
|
|
} else {
|
||
|
|
counter.textContent = target;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
updateCounter();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Form Validation
|
||
|
|
const formValidation = {
|
||
|
|
init() {
|
||
|
|
const forms = document.querySelectorAll('form');
|
||
|
|
forms.forEach(form => {
|
||
|
|
form.addEventListener('submit', (e) => {
|
||
|
|
if (!this.validateForm(form)) {
|
||
|
|
e.preventDefault();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Real-time validation
|
||
|
|
const inputs = form.querySelectorAll('input, textarea, select');
|
||
|
|
inputs.forEach(input => {
|
||
|
|
input.addEventListener('blur', () => {
|
||
|
|
this.validateField(input);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
validateForm(form) {
|
||
|
|
const inputs = form.querySelectorAll('[required]');
|
||
|
|
let isValid = true;
|
||
|
|
|
||
|
|
inputs.forEach(input => {
|
||
|
|
if (!this.validateField(input)) {
|
||
|
|
isValid = false;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return isValid;
|
||
|
|
},
|
||
|
|
|
||
|
|
validateField(field) {
|
||
|
|
const value = field.value.trim();
|
||
|
|
const type = field.type;
|
||
|
|
let isValid = true;
|
||
|
|
|
||
|
|
// Remove previous error
|
||
|
|
field.classList.remove('error');
|
||
|
|
const errorMsg = field.parentElement.querySelector('.error-message');
|
||
|
|
if (errorMsg) {
|
||
|
|
errorMsg.remove();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Required field
|
||
|
|
if (field.hasAttribute('required') && !value) {
|
||
|
|
this.showError(field, 'This field is required');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Email validation
|
||
|
|
if (type === 'email' && value) {
|
||
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
|
|
if (!emailRegex.test(value)) {
|
||
|
|
this.showError(field, 'Please enter a valid email address');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Phone validation
|
||
|
|
if (type === 'tel' && value) {
|
||
|
|
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
||
|
|
if (!phoneRegex.test(value)) {
|
||
|
|
this.showError(field, 'Please enter a valid phone number');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return isValid;
|
||
|
|
},
|
||
|
|
|
||
|
|
showError(field, message) {
|
||
|
|
field.classList.add('error');
|
||
|
|
const errorElement = document.createElement('div');
|
||
|
|
errorElement.className = 'error-message';
|
||
|
|
errorElement.textContent = message;
|
||
|
|
field.parentElement.appendChild(errorElement);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Yacht Quick View Modal
|
||
|
|
const yachtModal = {
|
||
|
|
init() {
|
||
|
|
const viewButtons = document.querySelectorAll('.yacht-view-btn');
|
||
|
|
viewButtons.forEach(btn => {
|
||
|
|
btn.addEventListener('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
this.openModal();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
openModal() {
|
||
|
|
// For now, just alert - in production, would open a modal
|
||
|
|
alert('Quick view modal would open here with yacht details and 360° view');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Parallax Effects
|
||
|
|
const parallaxEffects = {
|
||
|
|
init() {
|
||
|
|
this.elements = document.querySelectorAll('.parallax');
|
||
|
|
|
||
|
|
if (this.elements.length === 0) return;
|
||
|
|
|
||
|
|
window.addEventListener('scroll', () => {
|
||
|
|
this.updateParallax();
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
updateParallax() {
|
||
|
|
const scrolled = window.pageYOffset;
|
||
|
|
|
||
|
|
this.elements.forEach(element => {
|
||
|
|
const rate = element.dataset.rate || 0.5;
|
||
|
|
const yPos = -(scrolled * rate);
|
||
|
|
element.style.transform = `translateY(${yPos}px)`;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initialize everything when DOM is ready
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
themeManager.init();
|
||
|
|
navigation.init();
|
||
|
|
smoothScroll.init();
|
||
|
|
testimonialSlider.init();
|
||
|
|
counterAnimation.init();
|
||
|
|
formValidation.init();
|
||
|
|
yachtModal.init();
|
||
|
|
parallaxEffects.init();
|
||
|
|
|
||
|
|
// Log successful initialization
|
||
|
|
console.log('HarborSmith website initialized successfully!');
|
||
|
|
});
|