Initial import of HarborSmith website
Some checks failed
build-website / build (push) Failing after 1m2s

This commit is contained in:
2025-09-18 22:20:01 +02:00
commit ec72c5d62b
168 changed files with 65020 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
// HarborSmith - Animation Scripts
// ================================
// Scroll Animation Observer
const scrollAnimations = {
init() {
this.observeElements();
this.initProgressBars();
this.initTextAnimations();
},
observeElements() {
const options = {
threshold: 0.1,
rootMargin: '0px 0px -100px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
// Trigger specific animations
if (entry.target.classList.contains('counter-animate')) {
this.animateValue(entry.target);
}
// Only animate once
if (!entry.target.classList.contains('repeat-animation')) {
observer.unobserve(entry.target);
}
}
});
}, options);
// Observe all animated elements
const animatedElements = document.querySelectorAll('.scroll-animate, .scroll-animate-left, .scroll-animate-right, .scroll-animate-scale, .counter-animate');
animatedElements.forEach(el => observer.observe(el));
},
animateValue(element) {
const target = parseInt(element.dataset.target);
const duration = parseInt(element.dataset.duration) || 2000;
const start = 0;
const increment = target / (duration / 16);
let current = start;
const updateValue = () => {
current += increment;
if (current < target) {
element.textContent = Math.floor(current);
requestAnimationFrame(updateValue);
} else {
element.textContent = target;
}
};
updateValue();
},
initProgressBars() {
const progressBars = document.querySelectorAll('.progress-bar-fill');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const progressBar = entry.target;
const targetWidth = progressBar.dataset.progress || '100';
progressBar.style.setProperty('--progress', targetWidth + '%');
progressBar.classList.add('animate');
observer.unobserve(progressBar);
}
});
}, { threshold: 0.5 });
progressBars.forEach(bar => observer.observe(bar));
},
initTextAnimations() {
const textElements = document.querySelectorAll('.text-animate');
textElements.forEach(element => {
const text = element.textContent;
element.textContent = '';
[...text].forEach((char, index) => {
const span = document.createElement('span');
span.textContent = char === ' ' ? '\u00A0' : char;
span.style.animationDelay = `${index * 0.05}s`;
span.classList.add('char-animate');
element.appendChild(span);
});
});
}
};
// Cursor Effects
const cursorEffects = {
init() {
this.cursor = document.createElement('div');
this.cursor.className = 'custom-cursor';
document.body.appendChild(this.cursor);
this.follower = document.createElement('div');
this.follower.className = 'cursor-follower';
document.body.appendChild(this.follower);
this.initEventListeners();
},
initEventListeners() {
document.addEventListener('mousemove', (e) => {
this.cursor.style.left = e.clientX + 'px';
this.cursor.style.top = e.clientY + 'px';
setTimeout(() => {
this.follower.style.left = e.clientX + 'px';
this.follower.style.top = e.clientY + 'px';
}, 100);
});
// Add hover effects
const interactiveElements = document.querySelectorAll('a, button, .clickable');
interactiveElements.forEach(el => {
el.addEventListener('mouseenter', () => {
this.cursor.classList.add('hover');
this.follower.classList.add('hover');
});
el.addEventListener('mouseleave', () => {
this.cursor.classList.remove('hover');
this.follower.classList.remove('hover');
});
});
}
};
// Ripple Effect
const rippleEffect = {
init() {
const buttons = document.querySelectorAll('.btn, .ripple');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
const ripple = document.createElement('span');
ripple.className = 'ripple-effect';
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
button.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
});
}
};
// Magnetic Buttons
const magneticButtons = {
init() {
const magneticElements = document.querySelectorAll('.magnetic');
magneticElements.forEach(element => {
element.addEventListener('mousemove', (e) => {
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
element.style.transform = `translate(${x * 0.2}px, ${y * 0.2}px)`;
});
element.addEventListener('mouseleave', () => {
element.style.transform = 'translate(0, 0)';
});
});
}
};
// Page Loader
const pageLoader = {
init() {
const loader = document.createElement('div');
loader.className = 'page-loader';
loader.innerHTML = `
<div class="loader-content">
<div class="loader-ship">
<svg width="60" height="60" viewBox="0 0 60 60">
<path d="M30 10 L45 40 L15 40 Z" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M30 40 L30 50" stroke="currentColor" stroke-width="2"/>
<path d="M20 50 L40 50" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="loader-text">Setting Sail...</div>
</div>
`;
document.body.appendChild(loader);
window.addEventListener('load', () => {
setTimeout(() => {
loader.classList.add('fade-out');
setTimeout(() => {
loader.remove();
}, 500);
}, 1000);
});
}
};
// Smooth Page Transitions
const pageTransitions = {
init() {
const links = document.querySelectorAll('a[href^="/"], a[href^="./"], a[href$=".html"]');
links.forEach(link => {
link.addEventListener('click', (e) => {
const href = link.getAttribute('href');
// Skip if it's an anchor link or external link
if (href.startsWith('#') || href.startsWith('http')) return;
e.preventDefault();
document.body.classList.add('page-transition');
setTimeout(() => {
window.location.href = href;
}, 500);
});
});
}
};
// Initialize animations
document.addEventListener('DOMContentLoaded', () => {
scrollAnimations.init();
rippleEffect.init();
magneticButtons.init();
// Uncomment these for production
// cursorEffects.init();
// pageLoader.init();
// pageTransitions.init();
});
// Add some CSS for the loader (normally would be in a separate file)
const loaderStyles = `
<style>
.page-loader {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.5s;
}
.page-loader.fade-out {
opacity: 0;
pointer-events: none;
}
.loader-content {
text-align: center;
color: white;
}
.loader-ship {
animation: float 2s ease-in-out infinite;
margin-bottom: 20px;
}
.loader-text {
font-size: 1.5rem;
font-weight: 600;
animation: pulse 2s ease-in-out infinite;
}
.custom-cursor {
width: 10px;
height: 10px;
background: var(--primary);
border-radius: 50%;
position: fixed;
pointer-events: none;
z-index: 9998;
transform: translate(-50%, -50%);
transition: transform 0.2s, background 0.2s;
}
.cursor-follower {
width: 30px;
height: 30px;
border: 2px solid var(--primary);
border-radius: 50%;
position: fixed;
pointer-events: none;
z-index: 9997;
transform: translate(-50%, -50%);
transition: transform 0.3s, border-color 0.2s;
}
.custom-cursor.hover,
.cursor-follower.hover {
transform: translate(-50%, -50%) scale(1.5);
}
.ripple-effect {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
animation: ripple 0.6s ease-out;
pointer-events: none;
}
.char-animate {
display: inline-block;
opacity: 0;
animation: fadeInUp 0.5s ease-out forwards;
}
.page-transition {
animation: fadeOut 0.5s ease-out;
}
@keyframes fadeOut {
to {
opacity: 0;
transform: translateY(20px);
}
}
</style>
`;
// Inject loader styles
document.head.insertAdjacentHTML('beforeend', loaderStyles);

View File

@@ -0,0 +1,375 @@
// Bento Grid Layout JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Menu Toggle
const menuToggle = document.getElementById('menuToggle');
const menuOverlay = document.getElementById('menuOverlay');
const menuClose = document.getElementById('menuClose');
menuToggle.addEventListener('click', () => {
menuOverlay.classList.add('active');
});
menuClose.addEventListener('click', () => {
menuOverlay.classList.remove('active');
});
// Filter Toggle
const filterToggle = document.getElementById('filterToggle');
const filterBar = document.getElementById('filterBar');
filterToggle.addEventListener('click', () => {
filterBar.classList.toggle('active');
});
// Filter Functionality
const filterChips = document.querySelectorAll('.filter-chip');
const bentoTiles = document.querySelectorAll('.bento-tile');
filterChips.forEach(chip => {
chip.addEventListener('click', () => {
// Update active state
filterChips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
const filter = chip.dataset.filter;
// Filter tiles
bentoTiles.forEach(tile => {
if (filter === 'all' || tile.dataset.category === filter) {
tile.classList.remove('hiding');
tile.classList.add('showing');
} else {
tile.classList.add('hiding');
tile.classList.remove('showing');
}
});
});
});
// FAB Menu
const fabBtn = document.getElementById('fabBtn');
const fabMenu = document.getElementById('fabMenu');
fabBtn.addEventListener('click', () => {
fabMenu.classList.toggle('active');
});
// FAB Options
const fabOptions = document.querySelectorAll('.fab-option');
fabOptions.forEach(option => {
option.addEventListener('click', () => {
const action = option.dataset.action;
switch(action) {
case 'chat':
alert('Opening chat...');
break;
case 'call':
window.location.href = 'tel:4155550123';
break;
case 'book':
window.location.href = 'charter.html';
break;
}
fabMenu.classList.remove('active');
});
});
// Quick View for Yachts
const quickViewBtns = document.querySelectorAll('.quick-view-btn');
quickViewBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const yachtTile = btn.closest('.tile-yacht');
const yachtName = yachtTile.querySelector('.yacht-name').textContent;
// Create modal
const modal = document.createElement('div');
modal.className = 'yacht-modal';
modal.innerHTML = `
<div class="modal-content">
<button class="modal-close">&times;</button>
<h2>${yachtName}</h2>
<div class="modal-gallery">
<img src="${yachtTile.querySelector('img').src}" alt="${yachtName}">
</div>
<div class="modal-details">
<h3>Specifications</h3>
<ul>
<li>Length: 120ft</li>
<li>Capacity: 12 Guests</li>
<li>Cabins: 5</li>
<li>Crew: Full Professional Crew</li>
<li>Amenities: Jacuzzi, Water Toys, Full Bar</li>
</ul>
<div class="modal-actions">
<button class="btn-primary">Book Now</button>
<button class="btn-secondary">Request Info</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Add styles
const modalStyles = `
.yacht-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
animation: fadeIn 0.3s ease;
}
.modal-content {
background: white;
border-radius: 20px;
padding: 2rem;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
position: relative;
animation: slideUp 0.3s ease;
}
.modal-close {
position: absolute;
top: 1rem;
right: 1rem;
background: transparent;
border: none;
font-size: 2rem;
cursor: pointer;
}
.modal-gallery img {
width: 100%;
border-radius: 10px;
margin: 1rem 0;
}
.modal-details h3 {
margin: 1.5rem 0 1rem;
}
.modal-details ul {
list-style: none;
padding: 0;
}
.modal-details li {
padding: 0.5rem 0;
border-bottom: 1px solid #e2e8f0;
}
.modal-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.modal-actions button {
flex: 1;
padding: 0.75rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
if (!document.getElementById('modal-styles')) {
const styleSheet = document.createElement('style');
styleSheet.id = 'modal-styles';
styleSheet.textContent = modalStyles;
document.head.appendChild(styleSheet);
}
// Close modal
modal.querySelector('.modal-close').addEventListener('click', () => {
modal.remove();
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
});
});
// Wishlist functionality
let wishlist = [];
const wishlistSidebar = document.getElementById('wishlistSidebar');
const wishlistContent = document.getElementById('wishlistContent');
// Add to wishlist buttons (would be added to tiles)
bentoTiles.forEach(tile => {
if (tile.classList.contains('tile-yacht') || tile.classList.contains('tile-experience')) {
const wishlistBtn = document.createElement('button');
wishlistBtn.className = 'wishlist-add';
wishlistBtn.innerHTML = '♡';
wishlistBtn.style.cssText = `
position: absolute;
top: 1rem;
left: 1rem;
background: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
z-index: 10;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
`;
tile.appendChild(wishlistBtn);
wishlistBtn.addEventListener('click', (e) => {
e.stopPropagation();
const itemName = tile.querySelector('h3').textContent;
if (wishlistBtn.innerHTML === '♡') {
wishlistBtn.innerHTML = '❤️';
wishlist.push(itemName);
updateWishlist();
} else {
wishlistBtn.innerHTML = '♡';
wishlist = wishlist.filter(item => item !== itemName);
updateWishlist();
}
});
}
});
function updateWishlist() {
if (wishlist.length === 0) {
wishlistContent.innerHTML = '<p class="wishlist-empty">Select yachts and experiences to build your dream charter</p>';
} else {
wishlistContent.innerHTML = `
<h4>Selected Items (${wishlist.length})</h4>
<ul style="list-style: none; padding: 0;">
${wishlist.map(item => `
<li style="padding: 0.75rem; background: #f8fafc; margin-bottom: 0.5rem; border-radius: 8px;">
${item}
</li>
`).join('')}
</ul>
`;
}
}
// Show wishlist
document.addEventListener('keydown', (e) => {
if (e.key === 'w' && e.ctrlKey) {
e.preventDefault();
wishlistSidebar.classList.toggle('active');
}
});
document.getElementById('wishlistClose').addEventListener('click', () => {
wishlistSidebar.classList.remove('active');
});
// Parallax effect on scroll
let ticking = false;
function updateParallax() {
const scrolled = window.pageYOffset;
const heroTile = document.querySelector('.tile-hero');
if (heroTile) {
const heroVisual = heroTile.querySelector('.hero-visual');
if (heroVisual) {
heroVisual.style.transform = `translateY(${scrolled * 0.3}px)`;
}
}
ticking = false;
}
function requestTick() {
if (!ticking) {
window.requestAnimationFrame(updateParallax);
ticking = true;
}
}
window.addEventListener('scroll', requestTick);
// Auto-play/pause videos on visibility
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const video = entry.target.querySelector('video');
if (video) {
if (entry.isIntersecting) {
video.play();
} else {
video.pause();
}
}
});
}, { threshold: 0.5 });
document.querySelectorAll('.tile-video, .tile-hero').forEach(tile => {
observer.observe(tile);
});
// Tile click navigation
bentoTiles.forEach(tile => {
if (!tile.classList.contains('tile-yacht')) {
tile.addEventListener('click', () => {
if (tile.classList.contains('tile-service')) {
window.location.href = 'maintenance.html';
} else if (tile.classList.contains('tile-experience')) {
window.location.href = 'charter.html';
} else if (tile.classList.contains('tile-cta')) {
window.location.href = 'charter.html';
}
});
}
});
// Smooth hover effects
bentoTiles.forEach(tile => {
tile.addEventListener('mouseenter', () => {
tile.style.zIndex = '10';
});
tile.addEventListener('mouseleave', () => {
setTimeout(() => {
tile.style.zIndex = '';
}, 300);
});
});
});

View File

@@ -0,0 +1,89 @@
// HarborSmith - Booking JavaScript
// ================================
// Calendar functionality
document.addEventListener('DOMContentLoaded', () => {
// Calendar day selection
const calendarDays = document.querySelectorAll('.calendar-day.available');
calendarDays.forEach(day => {
day.addEventListener('click', function() {
// Remove previous selection
document.querySelector('.calendar-day.selected')?.classList.remove('selected');
// Add selection to clicked day
this.classList.add('selected');
// Update summary
updateBookingSummary();
});
});
// Duration selection
const durationOptions = document.querySelectorAll('input[name="duration"]');
durationOptions.forEach(option => {
option.addEventListener('change', updateBookingSummary);
});
// Guest number input
const guestInput = document.querySelector('input[type="number"]');
if (guestInput) {
guestInput.addEventListener('change', updateBookingSummary);
}
// Yacht selection (for booking page 2)
const yachtCards = document.querySelectorAll('.yacht-select-card');
yachtCards.forEach(card => {
card.addEventListener('click', function() {
// Remove previous selection
document.querySelector('.yacht-select-card.selected')?.classList.remove('selected');
// Add selection
this.classList.add('selected');
updateBookingSummary();
});
});
// Addon selection (for booking page 3)
const addonCards = document.querySelectorAll('.addon-card');
addonCards.forEach(card => {
card.addEventListener('click', function() {
this.classList.toggle('selected');
updateBookingSummary();
});
});
});
// Update booking summary
function updateBookingSummary() {
// This would normally update the summary sidebar with selected options
console.log('Updating booking summary...');
}
// Form validation for final step
function validateBookingForm() {
const form = document.getElementById('bookingForm');
if (!form) return true;
const inputs = form.querySelectorAll('[required]');
let isValid = true;
inputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('error');
isValid = false;
} else {
input.classList.remove('error');
}
});
return isValid;
}
// Handle form submission
const bookingForm = document.getElementById('bookingForm');
if (bookingForm) {
bookingForm.addEventListener('submit', (e) => {
e.preventDefault();
if (validateBookingForm()) {
alert('Thank you for your booking request! We will contact you shortly to confirm your charter.');
// In production, this would submit to a server
}
});
}

View 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!');
});

View File

@@ -0,0 +1,355 @@
// Voyage Layout JavaScript - Smooth, Interactive, Engaging
document.addEventListener('DOMContentLoaded', function() {
// Theme Switcher
const themeToggle = document.getElementById('themeToggle');
const themeDropdown = document.getElementById('themeDropdown');
const themeOptions = document.querySelectorAll('.theme-option');
if (themeToggle && themeDropdown) {
themeToggle.addEventListener('click', (e) => {
e.stopPropagation();
themeDropdown.classList.toggle('active');
});
themeOptions.forEach(option => {
option.addEventListener('click', () => {
const theme = option.dataset.theme;
document.body.className = theme === 'nautical' ? '' : `theme-${theme}`;
localStorage.setItem('selectedTheme', theme);
themeDropdown.classList.remove('active');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', () => {
themeDropdown.classList.remove('active');
});
// Load saved theme
const savedTheme = localStorage.getItem('selectedTheme');
if (savedTheme && savedTheme !== 'nautical') {
document.body.className = `theme-${savedTheme}`;
}
}
// Navigation scroll effect
const nav = document.getElementById('voyageNav');
let lastScroll = 0;
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
if (currentScroll > 50) {
nav.classList.add('scrolled');
} else {
nav.classList.remove('scrolled');
}
lastScroll = currentScroll;
});
// Smooth scroll for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Parallax effect for hero video
const heroSection = document.getElementById('heroSection');
const heroVideo = document.querySelector('.hero-video-container');
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
const rate = scrolled * 0.5;
if (heroVideo) {
heroVideo.style.transform = `translateY(${rate}px)`;
}
});
// Fleet Carousel
const yachtCards = document.querySelectorAll('.yacht-card');
const dots = document.querySelectorAll('.dot');
const prevBtn = document.querySelector('.fleet-prev');
const nextBtn = document.querySelector('.fleet-next');
let currentYacht = 0;
function showYacht(index) {
yachtCards.forEach(card => card.classList.remove('active'));
dots.forEach(dot => dot.classList.remove('active'));
yachtCards[index].classList.add('active');
dots[index].classList.add('active');
}
if (prevBtn && nextBtn) {
prevBtn.addEventListener('click', () => {
currentYacht = (currentYacht - 1 + yachtCards.length) % yachtCards.length;
showYacht(currentYacht);
});
nextBtn.addEventListener('click', () => {
currentYacht = (currentYacht + 1) % yachtCards.length;
showYacht(currentYacht);
});
}
dots.forEach((dot, index) => {
dot.addEventListener('click', () => {
currentYacht = index;
showYacht(currentYacht);
});
});
// Auto-rotate fleet carousel
setInterval(() => {
if (document.visibilityState === 'visible') {
currentYacht = (currentYacht + 1) % yachtCards.length;
showYacht(currentYacht);
}
}, 5000);
// CTA Button interactions
document.querySelectorAll('.btn-primary-warm, .btn-secondary-warm').forEach(btn => {
btn.addEventListener('click', function(e) {
// Create ripple effect
const ripple = document.createElement('span');
ripple.classList.add('ripple');
this.appendChild(ripple);
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
setTimeout(() => ripple.remove(), 600);
});
});
// Add ripple styles
const style = document.createElement('style');
style.textContent = `
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
transform: scale(0);
animation: ripple 0.6s ease-out;
pointer-events: none;
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
.btn-primary-warm,
.btn-secondary-warm {
position: relative;
overflow: hidden;
}
`;
document.head.appendChild(style);
// Animate elements on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -100px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Observe elements for animation
document.querySelectorAll('.welcome-content, .story-card, .booking-card').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'all 0.6s ease';
observer.observe(el);
});
// Interactive booking cards
document.querySelectorAll('.booking-card').forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-10px) scale(1.02)';
});
card.addEventListener('mouseleave', function() {
if (this.classList.contains('featured')) {
this.style.transform = 'scale(1.05)';
} else {
this.style.transform = 'translateY(0) scale(1)';
}
});
});
// Phone number click handler
document.querySelectorAll('.btn-booking.primary').forEach(btn => {
if (btn.textContent.includes('415')) {
btn.addEventListener('click', () => {
window.location.href = 'tel:4155550123';
});
}
});
// Video optimization - pause when not visible
const video = document.querySelector('.hero-video');
if (video) {
const videoObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
video.play();
} else {
video.pause();
}
});
}, { threshold: 0.25 });
videoObserver.observe(video);
}
// Story cards hover effect
document.querySelectorAll('.story-card').forEach(card => {
const img = card.querySelector('img');
card.addEventListener('mouseenter', () => {
if (img) {
img.style.transform = 'scale(1.1)';
img.style.transition = 'transform 0.6s ease';
}
});
card.addEventListener('mouseleave', () => {
if (img) {
img.style.transform = 'scale(1)';
}
});
});
// Dynamic trust badge counter - starts from 0
// Target the white text span specifically (the second span in trust-badge)
const trustBadge = document.querySelector('.trust-badge > span:last-child');
if (trustBadge) {
let count = 0;
const targetCount = 500;
const duration = 2000; // 2 seconds
const steps = 50;
const increment = targetCount / steps;
const stepDuration = duration / steps;
// Start with 0
trustBadge.textContent = `Trusted by 0+ adventurers`;
const counter = setInterval(() => {
count += increment;
if (count >= targetCount) {
count = targetCount;
clearInterval(counter);
trustBadge.textContent = `Trusted by 500+ adventurers`;
} else {
trustBadge.textContent = `Trusted by ${Math.floor(count)}+ adventurers`;
}
}, stepDuration);
}
// Remove any duplicate red text that might exist
const allSpans = document.querySelectorAll('.hero-content span');
allSpans.forEach(span => {
// Check if this is the red duplicate text (not inside trust-badge)
if (span.textContent.includes('Trusted by') &&
span.textContent.includes('adventurers') &&
!span.closest('.trust-badge')) {
span.style.display = 'none';
}
});
// Smooth hover for navigation links
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
});
link.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
// Mobile menu (would need to add hamburger menu for production)
const mobileMenuBtn = document.createElement('button');
mobileMenuBtn.className = 'mobile-menu-btn';
mobileMenuBtn.innerHTML = '<span></span><span></span><span></span>';
mobileMenuBtn.style.cssText = `
display: none;
flex-direction: column;
gap: 4px;
background: transparent;
border: none;
cursor: pointer;
padding: 0.5rem;
`;
// Add mobile menu styles
const mobileStyles = document.createElement('style');
mobileStyles.textContent = `
@media (max-width: 768px) {
.mobile-menu-btn {
display: flex !important;
}
.mobile-menu-btn span {
width: 24px;
height: 2px;
background: white;
transition: all 0.3s ease;
}
.voyage-nav.scrolled .mobile-menu-btn span {
background: var(--primary-blue);
}
}
`;
document.head.appendChild(mobileStyles);
// Add mobile menu button to nav
const navContainer = document.querySelector('.nav-container');
if (navContainer && window.innerWidth <= 768) {
navContainer.appendChild(mobileMenuBtn);
}
// Loading animation for images
document.querySelectorAll('img').forEach(img => {
// Check if image is already loaded
if (img.complete && img.naturalHeight !== 0) {
img.style.opacity = '1';
} else {
img.style.opacity = '0';
img.addEventListener('load', function() {
this.style.opacity = '1';
});
}
img.style.transition = 'opacity 0.6s ease';
});
console.log('Voyage layout initialized successfully!');
});