diff --git a/src/components/home-page/HeroVideoInline.tsx b/src/components/home-page/HeroVideoInline.tsx new file mode 100644 index 0000000..d2bee76 --- /dev/null +++ b/src/components/home-page/HeroVideoInline.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Maximize2 } from "lucide-react"; + +interface HeroVideoInlineProps { + sources: string[]; + poster?: string; + onExpand?: () => void; + className?: string; + aspectClassName?: string; +} + +// 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", +}) => { + 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 index 1c88f2e..65ba693 100644 --- a/src/components/home-page/HeroVideoModal.tsx +++ b/src/components/home-page/HeroVideoModal.tsx @@ -1,14 +1,22 @@ import React, { useEffect, useRef, useState } from "react"; import { X } from "lucide-react"; +interface HeroVideoStep { + number?: number; + title: string; + subtitle?: string; +} + interface HeroVideoModalProps { sources: string[]; + steps?: HeroVideoStep[]; isOpen: boolean; onClose: () => void; } const HeroVideoModal: React.FC = ({ sources, + steps, isOpen, onClose, }) => { @@ -46,6 +54,7 @@ const HeroVideoModal: React.FC = ({ if (!isOpen) return null; const currentSrc = sources[index]; + const hasSteps = !!steps && steps.length === sources.length; return (
= ({ aria-label="Product walkthrough video" >
e.stopPropagation()} >
); diff --git a/src/components/home-page/HomeHeroVibe.tsx b/src/components/home-page/HomeHeroVibe.tsx index 002e0b4..4a98d47 100644 --- a/src/components/home-page/HomeHeroVibe.tsx +++ b/src/components/home-page/HomeHeroVibe.tsx @@ -2,6 +2,7 @@ import React, { useRef, useState, useEffect } from "react"; import { Globe, AppWindow, + FlaskConical, Landmark, ShoppingCart, Play, @@ -9,6 +10,7 @@ import { Sparkles, ChevronDown, Check, + CheckCircle2, } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -16,7 +18,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 +74,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); @@ -135,10 +137,9 @@ const HomeHeroVibe = () => { const triggerDemo = DEMO_SCENARIOS.includes(appType) ? appType : AppType.E_COMMERCE; - const TriggerIcon = appTemplates[triggerDemo].icon; return ( -
+
{ }} /> -
- +

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

-
-
+
+ {/* Faint demo video backdrop behind the content. */} + setVideoOpen(true)} + className="col-start-1 row-start-1 z-0 w-[104%]" + aspectClassName="aspect-video" + /> +
+

+ {"Enter a URL, choose what to test, and Wopee's AI agent creates runnable tests with screenshots, checks, and self-healing steps."} +

+
- +
+ + {/* App selector — segmented "Your app" / "Demo app" toggle. + "Demo app" switches to a demo and opens the picker so a + specific demo (Website / E-commerce / Banking) stays + selectable. */} +
+
handleAppTypeChange(AppType.YOUR_APPLICATION)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleAppTypeChange(AppType.YOUR_APPLICATION); + } + }} + className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold leading-none cursor-pointer select-none transition-all ${ + appType === AppType.YOUR_APPLICATION + ? "bg-secondary-wopee text-white shadow-sm" + : "text-secondary-wopee/70 hover:text-secondary-wopee hover:bg-secondary-wopee/10 dark:text-white/70 dark:hover:text-white dark:hover:bg-white/10" + }`} + > + + Your app +
+
+
{ + if (!DEMO_SCENARIOS.includes(appType)) { + handleAppTypeChange(triggerDemo); + } + setDemoMenuOpen((open) => !open); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + if (!DEMO_SCENARIOS.includes(appType)) { + handleAppTypeChange(triggerDemo); + } + setDemoMenuOpen((open) => !open); + } + }} + className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-semibold leading-none cursor-pointer select-none transition-all ${ + DEMO_SCENARIOS.includes(appType) + ? "bg-secondary-wopee text-white shadow-sm" + : "text-secondary-wopee/70 hover:text-secondary-wopee hover:bg-secondary-wopee/10 dark:text-white/70 dark:hover:text-white dark:hover:bg-white/10" + }`} + > + + Demo app + +
+ {demoMenuOpen && ( +
+ {DEMO_SCENARIOS.map((type) => { + const tpl = appTemplates[type]; + const Icon = tpl.icon; + const selected = appType === type; + return ( +
handleAppTypeChange(type)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleAppTypeChange(type); + } + }} + className={`flex items-center gap-2 px-3 py-1.5 text-[11px] cursor-pointer select-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 ${selected ? "text-secondary-wopee dark:text-primary-wopee font-semibold" : "text-gray-700 dark:text-gray-300"}`} + > + + {tpl.label} + {selected && ( + + )} +
+ ); + })} +
+ )} +
+
+
{ })()}
-
- {/* Your app chip on the left, then "or try a demos:" label, - then a single expander showing the selected demo. Clicking - it reveals the three demo options; picking one pre-fills URL - + instructions and collapses the menu. One click on "Your - app" returns to the empty custom-URL state. */} -
- {(() => { - const tpl = appTemplates[AppType.YOUR_APPLICATION]; - const Icon = tpl.icon; - // "Your app" is only visually selected once the visitor has - // typed a URL in Your-app mode — otherwise the chip would - // read as active on landing while the CTA is disabled. - const selected = - appType === AppType.YOUR_APPLICATION && appUrl.length > 0; - return ( -
- handleAppTypeChange(AppType.YOUR_APPLICATION) - } - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleAppTypeChange(AppType.YOUR_APPLICATION); - } - }} - className={`scenario-chip ${selected ? "scenario-chip--selected" : ""} inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-normal cursor-pointer select-none transition-colors`} - aria-label={tpl.label} - > - - {tpl.label} -
- ); - })()} - - or try a demos: - -
- - {demoMenuOpen && ( -
- {DEMO_SCENARIOS.map((type) => { - const tpl = appTemplates[type]; - const Icon = tpl.icon; - const selected = appType === type; - return ( -
handleAppTypeChange(type)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleAppTypeChange(type); - } - }} - className={`flex items-center gap-2 px-3 py-1.5 text-[11px] cursor-pointer select-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 ${selected ? "text-secondary-wopee dark:text-primary-wopee font-semibold" : "text-gray-700 dark:text-gray-300"}`} - > - - {tpl.label} - {selected && ( - - )} -
- ); - })} -
- )} -
-
- +
+
+ + + Free to start + + + + No credit card required + + + + Works with your Playwright setup + +
+ +
+
+ {/* Watch-demo affordance over the video below the form. */} +
setVideoOpen(true)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setVideoOpen(true); + } + }} + aria-label="Watch 90-second demo" + className="group/play inline-flex items-center gap-2.5 cursor-pointer text-white/60 transition-colors duration-300 hover:text-white" + > + + + + Watch 90-sec demo
@@ -383,6 +429,7 @@ const HomeHeroVibe = () => { setVideoOpen(false)} /> diff --git a/src/css/custom.css b/src/css/custom.css index d190ed1..0619c90 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -397,6 +397,9 @@ body.is-home .navbar.navbar--revealed { background-color: rgb(0 0 0 / 0.05); color: rgb(75 85 99); /* gray-600 */ border: 1px solid rgb(0 0 0 / 0.08); + /* Normalise line-height so the
"Your app" chip and the