All checks were successful
build-website / build (push) Successful in 1m42s
- Remove height: 100% from HTML element that was preventing safe area extension - Add min-height with -webkit-fill-available instead - Detect iOS devices and set body position:fixed for proper safe area handling - iOS Safari requires body to be position:fixed for content to extend into safe areas This addresses the root cause: parent element constraints preventing video extension
371 lines
10 KiB
Vue
371 lines
10 KiB
Vue
<template>
|
|
<section class="hero-voyage" id="heroSection">
|
|
<!-- Video is now injected directly into DOM to bypass Vue/Nuxt issues -->
|
|
|
|
<div class="hero-content" :class="{ 'is-ready': videoLoaded }">
|
|
<div class="hero-main">
|
|
<div class="hero-logo animate-fade-in">
|
|
<img src="/HARBOR-SMITH-white.png" alt="Harbor Smith" width="400" height="250">
|
|
</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>
|
|
Reliable Care Above and Below the Waterline. Servicing the Bay Area and Beyond!
|
|
</p>
|
|
|
|
<div class="hero-actions animate-fade-up-delay-2">
|
|
<button class="btn-primary-warm" @click="handlePhoneClick">
|
|
<Icon name="lucide:phone" class="btn-icon" />
|
|
Call (510) 701-2535
|
|
</button>
|
|
<button class="btn-secondary-warm" @click="handleServicesClick">
|
|
<Icon name="lucide:wrench" class="btn-icon" />
|
|
View Our Services
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scroll to explore indicator -->
|
|
<div class="scroll-to-explore" @click="handleScrollToExplore">
|
|
<span>Scroll to explore</span>
|
|
<Icon name="lucide:chevron-down" class="chevron-icon" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } 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)
|
|
|
|
// Parallax will be disabled for injected video
|
|
// useParallax(videoContainer, 0.5)
|
|
useIntersectionAnimations()
|
|
useRipple()
|
|
|
|
const handleVideoLoaded = () => {
|
|
videoLoaded.value = true
|
|
}
|
|
|
|
const handlePhoneClick = () => {
|
|
window.location.href = 'tel:510-701-2535'
|
|
}
|
|
|
|
const handleServicesClick = () => {
|
|
scrollToElement('#services', 800) // Constant-speed smooth scrolling
|
|
}
|
|
|
|
const handleScrollToExplore = () => {
|
|
scrollToElement('#services', 800) // Constant-speed scroll to services
|
|
}
|
|
|
|
// Video visibility handling removed - video is now injected directly into DOM
|
|
|
|
const initSmoothScroll = () => {
|
|
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
|
anchor.addEventListener('click', (event) => {
|
|
event.preventDefault()
|
|
const href = anchor.getAttribute('href')
|
|
if (!href) {
|
|
return
|
|
}
|
|
scrollToElement(href, 800) // Constant-speed smooth 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(() => {
|
|
// FINAL FIX: iOS detection and body positioning for safe area extension
|
|
const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent);
|
|
|
|
if (isIOS) {
|
|
// iOS Safari requires body to be position:fixed for safe area extension
|
|
document.body.style.position = 'fixed';
|
|
document.body.style.width = '100%';
|
|
document.body.style.top = '0';
|
|
document.body.style.left = '0';
|
|
}
|
|
|
|
// Inject video with proper positioning
|
|
const videoHTML = `
|
|
<style>
|
|
#ios-video-bg {
|
|
position: fixed !important;
|
|
inset: 0 !important;
|
|
width: 100vw !important;
|
|
height: 100vh !important;
|
|
height: -webkit-fill-available !important;
|
|
z-index: -1 !important;
|
|
background: #000 !important;
|
|
}
|
|
|
|
@supports (-webkit-touch-callout: none) {
|
|
#ios-video-bg {
|
|
min-height: -webkit-fill-available !important;
|
|
}
|
|
}
|
|
|
|
#bg-video-element {
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
object-fit: cover !important;
|
|
object-position: center !important;
|
|
}
|
|
</style>
|
|
<div id="ios-video-bg">
|
|
<video
|
|
id="bg-video-element"
|
|
autoplay
|
|
muted
|
|
playsinline
|
|
loop>
|
|
<source src="https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4" type="video/mp4">
|
|
</video>
|
|
<div style="
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(to bottom,
|
|
rgba(0, 31, 63, 0.3) 0%,
|
|
rgba(0, 31, 63, 0.5) 50%,
|
|
rgba(0, 31, 63, 0.7) 100%);
|
|
"></div>
|
|
</div>
|
|
`;
|
|
|
|
// Insert at the very beginning of body, outside #__nuxt
|
|
document.body.insertAdjacentHTML('afterbegin', videoHTML);
|
|
|
|
// Get reference to injected video
|
|
const injectedVideo = document.getElementById('bg-video-element') as HTMLVideoElement;
|
|
|
|
// Handle video load state
|
|
if (injectedVideo) {
|
|
injectedVideo.addEventListener('loadeddata', () => {
|
|
videoLoaded.value = true;
|
|
});
|
|
|
|
// iOS autoplay fallback
|
|
const tryPlay = () => {
|
|
if (injectedVideo.paused) {
|
|
injectedVideo.play().catch(() => {});
|
|
}
|
|
};
|
|
window.addEventListener('pointerdown', tryPlay, { once: true });
|
|
window.addEventListener('touchstart', tryPlay, { once: true });
|
|
}
|
|
|
|
initSmoothScroll()
|
|
animateCount()
|
|
|
|
// Fallback: ensure video becomes visible after a delay
|
|
setTimeout(() => {
|
|
if (!videoLoaded.value) {
|
|
videoLoaded.value = true
|
|
}
|
|
}, 1500)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
// Clean up injected video on component unmount
|
|
document.getElementById('ios-video-bg')?.remove();
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Hero video container - COMPLETE safe area formula (Apple/Netflix pattern) */
|
|
.hero-video-container {
|
|
position: fixed; /* Fixed with complete formula works better */
|
|
/* Extend into ALL safe areas */
|
|
top: calc(0px - env(safe-area-inset-top, 0px));
|
|
left: calc(0px - env(safe-area-inset-left, 0px));
|
|
/* CRITICAL: Include ALL safe area insets in width and height */
|
|
width: calc(100vw + env(safe-area-inset-left, 0px) + env(safe-area-inset-right, 0px));
|
|
height: calc(100vh + env(safe-area-inset-top, 0px) + env(safe-area-inset-bottom, 0px));
|
|
z-index: 0; /* Base layer for video */
|
|
pointer-events: none;
|
|
overflow: hidden; /* Keep video content clipped */
|
|
/* Apply parallax transform using CSS variable - delayed to prevent autoplay issues */
|
|
transform: translateY(var(--parallax-offset, 0px));
|
|
will-change: transform; /* Optimization hint */
|
|
/* Initial state for autoplay compatibility */
|
|
transition: transform 0.1s ease-out;
|
|
}
|
|
|
|
/* iOS Safari safe area extension - removed to prevent interference with complete formula */
|
|
/* The main .hero-video-container rule now handles all safe areas properly */
|
|
|
|
/* Support for dynamic viewport height - simplified */
|
|
@supports (height: 100dvh) {
|
|
.hero-video-container {
|
|
/* Update height calc to include ALL safe areas for dvh */
|
|
height: calc(100dvh + env(safe-area-inset-top, 0px) + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
}
|
|
|
|
/* Video fade-in transition to prevent flash */
|
|
.hero-video {
|
|
opacity: 0;
|
|
transition: opacity 1s ease-in-out;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
object-position: center top;
|
|
/* Force GPU acceleration */
|
|
-webkit-transform: translateZ(0);
|
|
transform: translateZ(0);
|
|
}
|
|
|
|
.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 for hero section */
|
|
.hero-voyage {
|
|
position: static; /* Remove containing block to allow video to escape */
|
|
min-height: 100vh;
|
|
/* Allow video to extend into safe area */
|
|
overflow: visible;
|
|
isolation: isolate; /* Create new stacking context */
|
|
}
|
|
|
|
.hero-content {
|
|
position: fixed; /* Use fixed to match video container positioning */
|
|
top: 0; /* Start at viewport top */
|
|
left: 0;
|
|
right: 0;
|
|
width: 100%;
|
|
height: 100vh;
|
|
z-index: 10; /* Ensure content is above video */
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding-top: env(safe-area-inset-top, 0px); /* Safe area padding */
|
|
}
|
|
|
|
.hero-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
width: min(720px, 100%);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Scroll to explore indicator */
|
|
|
|
.scroll-to-explore {
|
|
margin-top: auto;
|
|
margin-bottom: calc(var(--safe-area-cover-bottom, 0px) + clamp(0.75rem, 2vh, 1.75rem));
|
|
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;
|
|
justify-self: center;
|
|
}
|
|
|
|
.scroll-to-explore:hover {
|
|
opacity: 1;
|
|
transform: 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: translateY(0);
|
|
}
|
|
40% {
|
|
transform: translateY(-10px);
|
|
}
|
|
60% {
|
|
transform: 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> |