// Floating portfolio strip — auto-loads whatever sits in assets/portfolio/. // Drop a .mp4/.webm/.gif into that folder on Hostinger and it shows up here. // list.php scans the folder (PHP runs on Hostinger). If it can't be reached // (e.g. a local preview with no PHP) it falls back to the local files below. const HERO_PORTFOLIO_DIR = 'assets/portfolio/'; const HERO_VIDEO_EXTS = ['mp4', 'webm', 'mov', 'm4v', 'ogv']; const HERO_FALLBACK = [ 'assets/portfolio/1.mp4', 'assets/portfolio/2.mp4', 'assets/portfolio/3.mp4', 'assets/portfolio/4.mp4', 'assets/portfolio/5.mp4', 'assets/portfolio/6.mp4', 'assets/portfolio/7.mp4', 'assets/portfolio/8.mp4', 'assets/portfolio/9.mp4', ]; function heroIsVideo(src) { return HERO_VIDEO_EXTS.includes((src.split('.').pop() || '').toLowerCase()); } // Lightweight poster (first-frame JPEG) for each video so the tile shows // instantly while the heavy clip streams in behind it. function heroPosterFor(src) { const i = src.lastIndexOf('/'); const dir = src.slice(0, i + 1); const file = src.slice(i + 1).replace(/\.[^.]+$/, ''); return dir + 'posters/' + file + '.jpg'; } function Hero() { const avatars = [ { src: 'assets/avatars/avatar-4.png' }, { src: 'assets/avatars/avatar-2.png' }, { src: 'assets/avatars/avatar-5.png' }, { src: 'assets/avatars/avatar-1.png' }, { src: 'assets/avatars/avatar-3.png' }, ]; const [portfolio, setPortfolio] = React.useState(HERO_FALLBACK); const marqueeRef = React.useRef(null); React.useEffect(() => { let cancelled = false; fetch(HERO_PORTFOLIO_DIR + 'list.php', { cache: 'no-cache' }) .then((r) => (r.ok ? r.json() : Promise.reject())) .then((files) => { if (!cancelled && Array.isArray(files) && files.length) { setPortfolio(files.map((f) => HERO_PORTFOLIO_DIR + f)); } }) .catch(() => {}); return () => { cancelled = true; }; }, []); // Playback management. Two jobs: // 1. Only play tiles actually on-screen; pause the rest — so the browser // isn't decoding all 18 looped videos at once (the cause of stutter). // 2. Self-heal: heavy clips sometimes fail to re-fire `loop` after a decode // stall and freeze on the last frame. An `ended` handler force-replays // them, so nothing stops after a single pass. React.useEffect(() => { const root = marqueeRef.current; if (!root) return; const vids = [...root.querySelectorAll('video')]; // Safety net: if a clip ends without looping, restart it. const onEnded = (e) => { const v = e.currentTarget; try { v.currentTime = 0; } catch (_) {} v.play().catch(() => {}); }; vids.forEach((v) => v.addEventListener('ended', onEnded)); const check = () => { const vw = window.innerWidth; const vh = window.innerHeight; vids.forEach((v) => { const r = v.getBoundingClientRect(); // generous margins so tiles don't thrash play/pause at the edges const visible = r.right > -160 && r.left < vw + 160 && r.bottom > -80 && r.top < vh + 80; if (visible) { // lazy-load: only fetch the heavy clip once the tile nears the screen if (!v.src && v.dataset.src) v.src = v.dataset.src; if (v.paused) v.play().catch(() => {}); } else if (!v.paused) { v.pause(); } }); }; check(); const id = setInterval(check, 400); return () => { clearInterval(id); vids.forEach((v) => v.removeEventListener('ended', onEnded)); }; }, [portfolio]); return (
{/* Horizon glow band — peak sits right at the marquee's top edge so it fades smoothly behind it */}
{avatars.map((a, i) => ( ))}
Loved by 100+ clients worldwide
Our clients speak for us
{/* Figma-style selection frame around the title + subtitle, with floating callout pills */}
{/* Dashed bounding frame */}
{/* Corner accent squares */} {[ { top: -4, left: -4 }, { top: -4, right: -4 }, { bottom: -4, left: -4 }, { bottom: -4, right: -4 }, ].map((pos, i) => (
))} {/* Floating callout pills — hidden below 1100px viewport to prevent edge clipping */} {/* Top-right: white "End-to-end" — hangs outside the right edge */}
End-to-end
{/* Mid-left: accent "Founder-led" — hangs outside the left edge */}
Founder-led
{/* Bottom-center: pale "Authority" — straddles the bottom frame edge */}
Authority

Generate 30 Days of Content In just 2 hours

One 2-hour shoot becomes 30 days of content that builds your authority
and attracts qualified leads — so you stay focused on closing.

Book A Call
{/* Floating portfolio marquee — sits inside the hero, just below the CTA */}
{[...portfolio, ...portfolio].map((src, i) => (
{heroIsVideo(src) ? (
))}
); } Object.assign(window, { Hero });