All checks were successful
build-website / build (push) Successful in 1m30s
Features added: - Custom smooth scroll composable with adjustable duration (1.5 seconds) - Replaced native browser smooth scroll with custom implementation - Added 'Scroll to explore' text with chevron at bottom of hero - Animated bounce effect for scroll indicator - Chevron icon with subtle animation - Click handler to scroll to services section - Hidden on mobile for cleaner experience All anchor links now scroll more slowly and smoothly for better UX.
283 lines
6.9 KiB
Vue
283 lines
6.9 KiB
Vue
<template>
|
|
<section class="hero-voyage" id="heroSection">
|
|
<div ref="videoContainer" class="hero-video-container">
|
|
<!-- White overlay that fades out when video is ready -->
|
|
<div
|
|
class="video-white-overlay"
|
|
:class="{ 'fade-out': videoLoaded }"
|
|
/>
|
|
|
|
<video
|
|
ref="videoElement"
|
|
autoplay
|
|
loop
|
|
muted
|
|
playsinline
|
|
preload="auto"
|
|
class="hero-video"
|
|
:class="{ 'video-loaded': videoLoaded }"
|
|
@loadeddata="handleVideoLoaded"
|
|
@canplaythrough="handleVideoLoaded"
|
|
@play="handleVideoLoaded"
|
|
>
|
|
<source
|
|
src="https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4"
|
|
type="video/mp4"
|
|
>
|
|
</video>
|
|
|
|
<div class="hero-overlay gradient-warm" />
|
|
<div class="hero-overlay gradient-depth" />
|
|
</div>
|
|
|
|
<div class="hero-content" :class="{ 'is-ready': videoLoaded }">
|
|
<div class="hero-logo animate-fade-in">
|
|
<img src="/HARBOR-SMITH-white.png" alt="Harbor Smith">
|
|
</div>
|
|
|
|
<div class="trust-badge animate-fade-in">
|
|
<div class="stars">
|
|
<span class="stars-icons">
|
|
<LucideStar class="star-filled" />
|
|
<LucideStar class="star-filled" />
|
|
<LucideStar class="star-filled" />
|
|
<LucideStar class="star-filled" />
|
|
<LucideStar class="star-filled" />
|
|
</span>
|
|
</div>
|
|
<span>Trusted by {{ animatedCount }}+ seafarers</span>
|
|
</div>
|
|
|
|
<p class="hero-subtext animate-fade-up-delay">
|
|
<span style="font-size: 1.5rem; font-weight: 500; text-transform: none; letter-spacing: normal; margin-bottom: 10px; display: block;">
|
|
Personalized Service Maintenance for Your Boat
|
|
</span>
|
|
Keep your vessel pristine with San Francisco Bay's premier mobile boat maintenance service.
|
|
</p>
|
|
|
|
<div class="hero-actions animate-fade-up-delay-2">
|
|
<button class="btn-primary-warm" @click="handlePhoneClick">
|
|
<LucidePhone class="btn-icon" />
|
|
Call (510) 701-2535
|
|
</button>
|
|
<button class="btn-secondary-warm" @click="handleServicesClick">
|
|
<LucideWrench class="btn-icon" />
|
|
View Our Services
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Scroll to explore indicator -->
|
|
<div class="scroll-to-explore" @click="handleScrollToExplore">
|
|
<span>Scroll to explore</span>
|
|
<LucideChevronDown class="chevron-icon" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useParallax } from '~/composables/useParallax'
|
|
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
|
|
import { useRipple } from '~/composables/useRipple'
|
|
import { useSmoothScroll } from '~/composables/useSmoothScroll'
|
|
|
|
const { scrollToElement } = useSmoothScroll()
|
|
const videoLoaded = ref(false)
|
|
const videoContainer = ref<HTMLElement | null>(null)
|
|
const videoElement = ref<HTMLVideoElement | null>(null)
|
|
const animatedCount = ref(0)
|
|
|
|
useParallax(videoContainer, 0.5)
|
|
useIntersectionAnimations()
|
|
useRipple()
|
|
|
|
const handleVideoLoaded = () => {
|
|
videoLoaded.value = true
|
|
}
|
|
|
|
const handlePhoneClick = () => {
|
|
window.location.href = 'tel:510-701-2535'
|
|
}
|
|
|
|
const handleServicesClick = () => {
|
|
scrollToElement('#services', 1500) // 1.5 seconds for slower scrolling
|
|
}
|
|
|
|
const handleScrollToExplore = () => {
|
|
scrollToElement('#services', 1500) // Scroll to services section
|
|
}
|
|
|
|
const handleVideoVisibility = () => {
|
|
if (!videoElement.value) {
|
|
return
|
|
}
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
videoElement.value?.play()
|
|
} else {
|
|
videoElement.value?.pause()
|
|
}
|
|
})
|
|
}, { threshold: 0.25 })
|
|
|
|
observer.observe(videoElement.value)
|
|
|
|
return () => observer.disconnect()
|
|
}
|
|
|
|
const initSmoothScroll = () => {
|
|
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
|
anchor.addEventListener('click', (event) => {
|
|
event.preventDefault()
|
|
const href = anchor.getAttribute('href')
|
|
if (!href) {
|
|
return
|
|
}
|
|
scrollToElement(href, 1500) // 1.5 seconds for slower scrolling
|
|
})
|
|
})
|
|
}
|
|
|
|
const animateCount = () => {
|
|
const duration = 1500 // 1.5 seconds
|
|
const targetCount = 100
|
|
const startTime = Date.now()
|
|
|
|
const updateCount = () => {
|
|
const elapsed = Date.now() - startTime
|
|
const progress = Math.min(elapsed / duration, 1)
|
|
|
|
// Easing function for smooth animation
|
|
const easeOutQuart = 1 - Math.pow(1 - progress, 4)
|
|
animatedCount.value = Math.floor(easeOutQuart * targetCount)
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(updateCount)
|
|
} else {
|
|
animatedCount.value = targetCount
|
|
}
|
|
}
|
|
|
|
// Start animation after a small delay for better effect
|
|
setTimeout(() => {
|
|
updateCount()
|
|
}, 500)
|
|
}
|
|
|
|
onMounted(() => {
|
|
handleVideoVisibility()
|
|
initSmoothScroll()
|
|
animateCount()
|
|
|
|
// Fallback: ensure video becomes visible after a delay
|
|
setTimeout(() => {
|
|
if (!videoLoaded.value) {
|
|
videoLoaded.value = true
|
|
}
|
|
}, 1500)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Video fade-in transition to prevent flash */
|
|
.hero-video {
|
|
opacity: 0;
|
|
transition: opacity 1s ease-in-out;
|
|
}
|
|
|
|
.hero-video.video-loaded {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* White overlay for smooth video loading transition */
|
|
.video-white-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
|
|
z-index: 2;
|
|
opacity: 1;
|
|
transition: opacity 800ms ease-out;
|
|
will-change: opacity;
|
|
}
|
|
|
|
.video-white-overlay.fade-out {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Container to prevent horizontal overflow */
|
|
.hero-voyage {
|
|
overflow-x: hidden !important;
|
|
max-width: 100vw !important;
|
|
}
|
|
|
|
/* Scroll to explore indicator */
|
|
.scroll-to-explore {
|
|
position: absolute;
|
|
bottom: 40px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
cursor: pointer;
|
|
color: white;
|
|
opacity: 0.8;
|
|
transition: all 0.3s ease;
|
|
animation: bounce 2s infinite;
|
|
}
|
|
|
|
.scroll-to-explore:hover {
|
|
opacity: 1;
|
|
transform: translateX(-50%) translateY(-3px);
|
|
}
|
|
|
|
.scroll-to-explore span {
|
|
font-size: 0.875rem;
|
|
font-weight: 300;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.chevron-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
animation: chevronBounce 2s infinite;
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0%, 20%, 50%, 80%, 100% {
|
|
transform: translateX(-50%) translateY(0);
|
|
}
|
|
40% {
|
|
transform: translateX(-50%) translateY(-10px);
|
|
}
|
|
60% {
|
|
transform: translateX(-50%) translateY(-5px);
|
|
}
|
|
}
|
|
|
|
@keyframes chevronBounce {
|
|
0%, 100% {
|
|
transform: translateY(0);
|
|
}
|
|
50% {
|
|
transform: translateY(5px);
|
|
}
|
|
}
|
|
|
|
/* Hide on mobile for better UX */
|
|
@media (max-width: 768px) {
|
|
.scroll-to-explore {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|