Revert "fix: restructure video as global backdrop for iOS notch extension"
All checks were successful
build-website / build (push) Successful in 1m34s

This reverts commit ed92c458a9.
This commit is contained in:
2025-09-21 13:54:03 +02:00
parent ed92c458a9
commit 509f7ac57b
4 changed files with 111 additions and 235 deletions

View File

@@ -1,33 +0,0 @@
<template>
<div>
<!-- Global video backdrop - sits behind all content -->
<VideoBackdrop />
<!-- Navigation -->
<AppNavbar />
<!-- Page content -->
<NuxtPage />
<!-- Footer -->
<AppFooter />
</div>
</template>
<script setup lang="ts">
// The VideoBackdrop component handles the full-viewport video
// that extends under the iOS notch
</script>
<style>
/* Ensure the app container doesn't clip the video */
body {
margin: 0;
padding: 0;
overflow-x: hidden;
}
html {
overflow-x: hidden;
}
</style>

View File

@@ -410,8 +410,6 @@ html {
/* Hero Section */ /* Hero Section */
.hero-voyage { .hero-voyage {
position: relative; position: relative;
/* Hero is now transparent - video handled by VideoBackdrop component */
background: transparent;
height: 100vh; /* Fallback for browsers that don't support newer units */ height: 100vh; /* Fallback for browsers that don't support newer units */
height: -webkit-fill-available; /* iOS Safari - fills available space */ height: -webkit-fill-available; /* iOS Safari - fills available space */
height: 100dvh; /* Dynamic viewport height */ height: 100dvh; /* Dynamic viewport height */
@@ -1676,9 +1674,6 @@ html {
/* Responsive */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.hero-voyage { .hero-voyage {
/* Hero is now transparent - video handled by VideoBackdrop component */
position: relative;
background: transparent;
padding: 0; padding: 0;
/* iOS-specific height handling */ /* iOS-specific height handling */
height: 100vh; height: 100vh;
@@ -1687,11 +1682,24 @@ html {
min-height: 100vh; min-height: 100vh;
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
min-height: 100dvh; min-height: 100dvh;
/* Video will extend under notch - no padding-top here */
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
overflow-x: hidden !important; overflow-x: hidden !important;
max-width: 100vw !important; max-width: 100vw !important;
} }
/* Make video container fixed on mobile to fill entire viewport including notch */
.hero-video-container {
position: fixed !important;
inset: 0;
width: 100vw;
height: 100vh;
height: -webkit-fill-available;
height: 100dvh;
z-index: 0;
pointer-events: none;
}
.hero-content { .hero-content {
/* Adjust padding to account for safe areas on all sides */ /* Adjust padding to account for safe areas on all sides */
padding: clamp(1rem, 4vw, 2rem); padding: clamp(1rem, 4vw, 2rem);

View File

@@ -1,7 +1,36 @@
<template> <template>
<section class="hero-voyage" id="heroSection"> <section class="hero-voyage" id="heroSection">
<!-- Video is now handled by VideoBackdrop component --> <div ref="videoContainer" class="hero-video-container">
<div class="hero-content"> <!-- 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"> <div class="hero-logo animate-fade-in">
<img src="/HARBOR-SMITH-white.png" alt="Harbor Smith" width="400" height="250"> <img src="/HARBOR-SMITH-white.png" alt="Harbor Smith" width="400" height="250">
</div> </div>
@@ -36,16 +65,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useParallax } from '~/composables/useParallax'
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations' import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
import { useRipple } from '~/composables/useRipple' import { useRipple } from '~/composables/useRipple'
import { useSmoothScroll } from '~/composables/useSmoothScroll' import { useSmoothScroll } from '~/composables/useSmoothScroll'
const { scrollToElement } = useSmoothScroll() const { scrollToElement } = useSmoothScroll()
const videoLoaded = ref(false)
const videoContainer = ref<HTMLElement | null>(null)
const videoElement = ref<HTMLVideoElement | null>(null)
const animatedCount = ref(0) const animatedCount = ref(0)
useParallax(videoContainer, 0.5)
useIntersectionAnimations() useIntersectionAnimations()
useRipple() useRipple()
const handleVideoLoaded = () => {
videoLoaded.value = true
}
const handlePhoneClick = () => { const handlePhoneClick = () => {
window.location.href = 'tel:510-701-2535' window.location.href = 'tel:510-701-2535'
} }
@@ -58,6 +96,26 @@ const handleScrollToExplore = () => {
scrollToElement('#services', 800) // Constant-speed scroll to services scrollToElement('#services', 800) // Constant-speed scroll to services
} }
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 = () => { const initSmoothScroll = () => {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => { document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener('click', (event) => { anchor.addEventListener('click', (event) => {
@@ -98,21 +156,53 @@ const animateCount = () => {
} }
onMounted(() => { onMounted(() => {
handleVideoVisibility()
initSmoothScroll() initSmoothScroll()
animateCount() animateCount()
// Fallback: ensure video becomes visible after a delay
setTimeout(() => {
if (!videoLoaded.value) {
videoLoaded.value = true
}
}, 1500)
}) })
</script> </script>
<style scoped> <style scoped>
/* Hero section is now transparent to show video backdrop behind it */ /* 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 { .hero-voyage {
position: relative;
background: transparent;
overflow-x: hidden !important; overflow-x: hidden !important;
max-width: 100vw !important; max-width: 100vw !important;
/* Ensure hero takes up full viewport height */
min-height: 100vh;
min-height: 100dvh;
} }
/* Scroll to explore indicator */ /* Scroll to explore indicator */

View File

@@ -1,189 +0,0 @@
<template>
<div class="video-backdrop" v-if="isHeroPage">
<div
ref="videoContainer"
class="video-layer"
:class="{ 'video-loaded': videoLoaded }"
>
<!-- 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"
@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>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const videoLoaded = ref(false)
const videoElement = ref<HTMLVideoElement | null>(null)
const videoContainer = ref<HTMLElement | null>(null)
// Only show on home page
const isHeroPage = computed(() => route.path === '/')
const handleVideoLoaded = () => {
videoLoaded.value = true
}
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()
}
onMounted(() => {
handleVideoVisibility()
// Fallback: ensure video becomes visible after a delay
setTimeout(() => {
if (!videoLoaded.value) {
videoLoaded.value = true
}
}, 1500)
})
</script>
<style scoped>
/* Video backdrop container - sits behind everything */
.video-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for iOS */
z-index: 0; /* Behind all content */
pointer-events: none; /* Don't block interactions */
overflow: hidden;
}
/* Video layer that extends into safe areas */
.video-layer {
position: absolute;
/* Extend into notch area on iOS */
top: calc(-1 * env(safe-area-inset-top, 0px));
left: 0;
right: 0;
width: 100vw;
/* Height includes the safe area extension */
height: calc(100dvh + env(safe-area-inset-top, 0px));
opacity: 0;
transition: opacity 1s ease-in-out;
}
.video-layer.video-loaded {
opacity: 1;
}
/* Video element */
.hero-video {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
object-fit: cover;
}
/* White overlay for smooth loading */
.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;
}
/* Gradient overlays */
.hero-overlay {
position: absolute;
inset: 0;
pointer-events: none;
}
.gradient-warm {
background: linear-gradient(
180deg,
rgba(0, 31, 63, 0.3) 0%,
rgba(180, 139, 78, 0.2) 50%,
rgba(157, 121, 67, 0.3) 100%
);
mix-blend-mode: multiply;
opacity: 0.7;
}
.gradient-depth {
background: radial-gradient(
ellipse at center,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.4) 100%
);
opacity: 0.5;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.video-backdrop {
height: 100vh;
height: -webkit-fill-available;
height: 100dvh;
}
.video-layer {
/* Ensure video extends into notch on mobile */
top: calc(-1 * env(safe-area-inset-top, 0px));
height: calc(100dvh + env(safe-area-inset-top, 0px));
}
}
</style>