diff --git a/src/components/home-page/HeroTrustedByStrip.tsx b/src/components/home-page/HeroTrustedByStrip.tsx index a30f08f..7cd0234 100644 --- a/src/components/home-page/HeroTrustedByStrip.tsx +++ b/src/components/home-page/HeroTrustedByStrip.tsx @@ -46,8 +46,8 @@ const HeroTrustedByStrip = () => { {customer.name} void; + className?: string; + aspectClassName?: string; + // When true (default) the clip sits as a faint grayscale backdrop and only + // comes to full colour on hover. When false it plays fully visible. + dimmed?: boolean; +} + +// Muted autoplay loop of the demo clips, cycling through `sources`. Muting is +// required for browsers to allow autoplay without a user gesture; onExpand +// opens the fullscreen modal with sound + controls. +const HeroVideoInline: React.FC = ({ + sources, + poster, + onExpand, + className = "", + aspectClassName = "aspect-video", + dimmed = true, +}) => { + const [index, setIndex] = useState(0); + const videoRef = useRef(null); + + useEffect(() => { + videoRef.current?.play().catch(() => {}); + }, [index]); + + const currentSrc = sources[index]; + + return ( +
+ {/* gradient frame, faded in on hover */} +
+ ); +}; + +export default HeroVideoInline; diff --git a/src/components/home-page/HeroVideoModal.tsx b/src/components/home-page/HeroVideoModal.tsx deleted file mode 100644 index 1c88f2e..0000000 --- a/src/components/home-page/HeroVideoModal.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { X } from "lucide-react"; - -interface HeroVideoModalProps { - sources: string[]; - isOpen: boolean; - onClose: () => void; -} - -const HeroVideoModal: React.FC = ({ - sources, - isOpen, - onClose, -}) => { - const [index, setIndex] = useState(0); - const videoRef = useRef(null); - - useEffect(() => { - if (isOpen) setIndex(0); - }, [isOpen]); - - useEffect(() => { - if (isOpen && videoRef.current) { - videoRef.current.load(); - videoRef.current.play().catch(() => {}); - } - }, [index, isOpen]); - - useEffect(() => { - if (!isOpen) return; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [isOpen, onClose]); - - const handleEnded = () => { - if (index < sources.length - 1) { - setIndex(index + 1); - } else { - onClose(); - } - }; - - if (!isOpen) return null; - - const currentSrc = sources[index]; - - return ( -
-
e.stopPropagation()} - > - - - - -
- {sources.map((_, i) => ( - - ))} -
-
-
- ); -}; - -export default HeroVideoModal; diff --git a/src/components/home-page/HomeHeroVibe.tsx b/src/components/home-page/HomeHeroVibe.tsx index 002e0b4..f4a4f42 100644 --- a/src/components/home-page/HomeHeroVibe.tsx +++ b/src/components/home-page/HomeHeroVibe.tsx @@ -2,9 +2,9 @@ import React, { useRef, useState, useEffect } from "react"; import { Globe, AppWindow, + FlaskConical, Landmark, ShoppingCart, - Play, Plus, Sparkles, ChevronDown, @@ -15,8 +15,9 @@ import { Button } from "@/components/ui/button"; import { AppType } from "./vibe/enums"; import LoginDialog from "./vibe/LoginDialog"; -import HeroVideoModal from "./HeroVideoModal"; +import HeroVideoInline from "./HeroVideoInline"; import HeroTrustedByStrip from "./HeroTrustedByStrip"; +import { stepsData } from "../../data/steps"; const appTemplates = { [AppType.WEBSITE]: { @@ -70,11 +71,9 @@ const DEMO_SCENARIOS: AppType[] = [ const URL_REGEX = /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/; -const heroVideoSources = [ - "/how-it-works/step-1.webm", - "/how-it-works/step-2.webm", - "/how-it-works/step-3.webm", -]; +// Same clips (and order) as the "How it works" section, so the fullscreen +// modal can label each fragment with its step title/subtitle. +const heroVideoSources = stepsData.map((s) => s.videoSrc); const HomeHeroVibe = () => { const [appUrl, setAppUrl] = useState(appTemplates[defaultTemplate].url); @@ -89,8 +88,8 @@ const HomeHeroVibe = () => { isOpen: false, mode: "vibe", }); - const [videoOpen, setVideoOpen] = useState(false); const [showInstructions, setShowInstructions] = useState(false); + const [videoHovered, setVideoHovered] = useState(false); const [demoMenuOpen, setDemoMenuOpen] = useState(false); const urlInputRef = useRef(null); const demoMenuRef = useRef(null); @@ -135,10 +134,9 @@ const HomeHeroVibe = () => { const triggerDemo = DEMO_SCENARIOS.includes(appType) ? appType : AppType.E_COMMERCE; - const TriggerIcon = appTemplates[triggerDemo].icon; return ( -
+
{ }} /> -
- +

AI Testing Agents @@ -183,13 +172,120 @@ const HomeHeroVibe = () => {

-
-
-
+
+ {/* Left: the "start testing" form. */} +
+
+ {/* Gradient border layer — animates out when the demo video is + hovered, mirroring the video's frame animating in. */} +