feat(ui): add ultra-optimized LazyVideoHero component with IntersectionObserver auto-pause, section-scoping, and CSS layout containment
This commit is contained in:
86
src/components/LazyVideoHero/LazyVideoHero.tsx
Normal file
86
src/components/LazyVideoHero/LazyVideoHero.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface LazyVideoHeroProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LazyVideoHero({ children }: LazyVideoHeroProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const [isIntersecting, setIsIntersecting] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
setIsIntersecting(entry.isIntersecting);
|
||||||
|
if (videoRef.current) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
videoRef.current.play().catch(() => {
|
||||||
|
// Ignore autoplay restrictions if browser blocks
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
videoRef.current.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.1, // Pause when 90% of section is out of view
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(container);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="relative w-full overflow-hidden">
|
||||||
|
{/* ─── Optimized Video Background ────────────────────────────────────────── */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 w-full h-full z-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
contain: 'strict',
|
||||||
|
willChange: 'transform',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
preload="metadata"
|
||||||
|
className="w-full h-full object-cover transition-opacity duration-700"
|
||||||
|
style={{
|
||||||
|
filter: 'contrast(1.08) brightness(0.85) saturate(1.05)',
|
||||||
|
opacity: isIntersecting ? 1 : 0,
|
||||||
|
contain: 'strict',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<source src="/video/whatsapp-video.mp4" type="video/mp4" />
|
||||||
|
<source src="/video/whatsapp-video.webm" type="video/webm" />
|
||||||
|
<source src="/video/RIT Video.mp4" type="video/mp4" />
|
||||||
|
<source src="/video/RIT Video.webm" type="video/webm" />
|
||||||
|
</video>
|
||||||
|
|
||||||
|
{/* High-Contrast Glassmorphic Overlay Gradient */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(to bottom, rgba(15, 23, 42, 0.45) 0%, rgba(15, 23, 42, 0.75) 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── Hero Content ──────────────────────────────────────────────────────── */}
|
||||||
|
<div className="relative z-10">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer'
|
|||||||
import { FEATURES, CAMPUS_LOCATIONS } from '@/constants';
|
import { FEATURES, CAMPUS_LOCATIONS } from '@/constants';
|
||||||
import * as LucideIcons from 'lucide-react';
|
import * as LucideIcons from 'lucide-react';
|
||||||
|
|
||||||
|
import LazyVideoHero from '@/components/LazyVideoHero/LazyVideoHero';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { scrollY } = useScroll();
|
const { scrollY } = useScroll();
|
||||||
const heroOpacity = useTransform(scrollY, [0, 450], [1, 0]);
|
const heroOpacity = useTransform(scrollY, [0, 450], [1, 0]);
|
||||||
@@ -18,26 +20,8 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative bg-[#FAFAFA] min-h-screen overflow-hidden">
|
<div className="relative bg-[#FAFAFA] min-h-screen overflow-hidden">
|
||||||
{/* ─── Ultra-Sleek Mesh Background Effect (Lightweight - Zero Video Load) ─── */}
|
{/* ─── Hero Section with IntersectionObserver Lazy Video ─────────────────── */}
|
||||||
<div className="absolute top-0 left-0 right-0 h-[650px] overflow-hidden pointer-events-none z-0">
|
<LazyVideoHero>
|
||||||
<div
|
|
||||||
className="absolute -top-[20%] -left-[10%] w-[60%] h-[80%] rounded-full opacity-20 blur-[120px]"
|
|
||||||
style={{ background: 'radial-gradient(circle, #F97316 0%, #FB923C 100%)' }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute top-[10%] -right-[10%] w-[50%] h-[70%] rounded-full opacity-15 blur-[120px]"
|
|
||||||
style={{ background: 'radial-gradient(circle, #EA580C 0%, #F97316 100%)' }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 opacity-[0.03]"
|
|
||||||
style={{
|
|
||||||
backgroundImage: 'radial-gradient(#1E293B 1px, transparent 1px)',
|
|
||||||
backgroundSize: '24px 24px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ─── Hero Section ─────────────────────────────────────────────────────── */}
|
|
||||||
<section className="relative pt-20 pb-24 z-10">
|
<section className="relative pt-20 pb-24 z-10">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="container-custom relative z-20 flex flex-col items-center text-center px-4"
|
className="container-custom relative z-20 flex flex-col items-center text-center px-4"
|
||||||
@@ -142,6 +126,7 @@ export default function Home() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
|
</LazyVideoHero>
|
||||||
|
|
||||||
{/* Sleek Stats Card Section */}
|
{/* Sleek Stats Card Section */}
|
||||||
<section className="relative z-30 -mt-6 px-4">
|
<section className="relative z-30 -mt-6 px-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user