Initial import of HarborSmith website
Some checks failed
build-website / build (push) Failing after 1m2s
Some checks failed
build-website / build (push) Failing after 1m2s
This commit is contained in:
339
website-mockups/js/main.js
Normal file
339
website-mockups/js/main.js
Normal file
@@ -0,0 +1,339 @@
|
||||
// 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!');
|
||||
});
|
||||
Reference in New Issue
Block a user