From b40f6260146890bbf0559ceafd22fba7132375d5 Mon Sep 17 00:00:00 2001 From: Marcel Veselka Date: Mon, 8 Jun 2026 17:13:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(home):=20two-column=20video=20hero=20?= =?UTF-8?q?=E2=80=94=20form=20beside=20a=20full-colour=20demo=20video?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the homepage hero into a two-column layout: the "start testing" form on the left, a full-colour autoplaying demo video on the right. This restores the demo-video hero from #230 (reverted in #232) and evolves it; rebased on main, so the reveal-on-scroll navbar removed in #233 stays out. - Two-column layout; HeroVideoInline gains a `dimmed` prop (default true). - Drop the video maximize/expand affordance, the "Watch 90-sec demo" link, and the now-unused HeroVideoModal (deleted). - Hovering the video fades the form's gradient border out as the video's own frame fades in (shared 500ms timing). - Tidy hero copy: remove the subtitle; replace the green trust row with a single "No credit card required" line under the CTA. - Trim the sign-in popup: drop the "Testing " line and the "Sign in › Agents test › Get your report" breadcrumb. - Calm the trusted-by marquee: lower logo opacity (70→40), widen the edge fade, slow the scroll (35s→90s) — keeps motion but recedes behind the video. - Responsive vertical spacing + wider form (480→560px) + taller instructions textarea (3→5 rows). --- .../home-page/HeroTrustedByStrip.tsx | 4 +- src/components/home-page/HeroVideoInline.tsx | 85 ++++++ src/components/home-page/HeroVideoModal.tsx | 97 ------ src/components/home-page/HomeHeroVibe.tsx | 281 ++++++++++-------- src/components/home-page/vibe/LoginDialog.tsx | 53 +--- src/css/custom.css | 6 +- 6 files changed, 243 insertions(+), 283 deletions(-) create mode 100644 src/components/home-page/HeroVideoInline.tsx delete mode 100644 src/components/home-page/HeroVideoModal.tsx 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. */} +