mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-08 22:22:17 +02:00
- Updated the pricing page to include improved SEO metadata, including title, description, and keywords. - Added structured data using JsonLd for better search engine visibility. - Revised pricing plan descriptions and features for clarity and accuracy. - Enhanced link behavior for external pricing links to open in a new tab with appropriate security attributes. - Updated FAQ sections to reflect changes in credit and billing explanations.
838 lines
25 KiB
TypeScript
838 lines
25 KiB
TypeScript
"use client";
|
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
import Link from "next/link";
|
|
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
import Balancer from "react-wrap-balancer";
|
|
import { HeroChatDemo, type HeroChatDemoScript } from "@/components/homepage/hero-chat-demo";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ExpandedMediaOverlay, useExpandedMedia } from "@/components/ui/expanded-gif-overlay";
|
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { buildBackendUrl } from "@/lib/env-config";
|
|
import { trackLoginAttempt } from "@/lib/posthog/events";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const GoogleLogo = ({ className }: { className?: string }) => (
|
|
<svg
|
|
className={className}
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
role="img"
|
|
aria-label="Google logo"
|
|
>
|
|
<title>Google logo</title>
|
|
<path
|
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
fill="#4285F4"
|
|
/>
|
|
<path
|
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
fill="#34A853"
|
|
/>
|
|
<path
|
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
fill="#FBBC05"
|
|
/>
|
|
<path
|
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
fill="#EA4335"
|
|
/>
|
|
</svg>
|
|
);
|
|
|
|
type HeroUseCase = {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
src: string | null;
|
|
/** Scripted chat demo shown when there is no recorded video. */
|
|
demo?: HeroChatDemoScript;
|
|
};
|
|
|
|
type HeroCategory = {
|
|
id: string;
|
|
label: string;
|
|
useCases: HeroUseCase[];
|
|
};
|
|
|
|
const HERO_TUTORIAL = "/homepage/hero_tutorial";
|
|
const HERO_REALTIME = "/homepage/hero_realtime";
|
|
|
|
/*
|
|
* Every scripted demo below mirrors a task the SurfSense agent has actually run
|
|
* end-to-end (see backend agent e2e suite). Recorded videos take precedence via
|
|
* `src`; everything else plays the chat demo.
|
|
*/
|
|
const CATEGORIES: HeroCategory[] = [
|
|
{
|
|
id: "competitor-monitoring",
|
|
label: "Competitor Monitoring",
|
|
useCases: [
|
|
{
|
|
id: "pricing-watch",
|
|
title: "Competitor Pricing Watch",
|
|
description:
|
|
"The agent extracts every plan from a competitor's pricing page, and an automation re-checks it so you hear about changes first.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Extract every plan, price, and limit from our top 3 competitors' pricing pages.",
|
|
steps: [
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Crawl 3 pricing pages", "Extract plans, prices, limits into one table"],
|
|
},
|
|
{
|
|
title: "Web Crawler",
|
|
items: [
|
|
"competitor-a.com/pricing · 4 plans",
|
|
"competitor-b.com/pricing · 3 plans",
|
|
"competitor-c.com/plans · 4 plans",
|
|
],
|
|
},
|
|
{
|
|
title: "Create automation",
|
|
items: ["Daily pricing re-check · alert on any change"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "Competitor A — Pro $49/mo",
|
|
secondary: "10k credits · 3 seats · raised from $39 on Jun 12",
|
|
},
|
|
{
|
|
primary: "Competitor B — Team $99/mo",
|
|
secondary: "50k credits · unlimited seats · annual-only",
|
|
},
|
|
{
|
|
primary: "Competitor C — Free tier removed",
|
|
secondary: "Trial now 7 days · card required",
|
|
},
|
|
],
|
|
summary: "3 pages parsed · 11 plans in one table · daily re-check scheduled",
|
|
},
|
|
},
|
|
{
|
|
id: "site-diff",
|
|
title: "Product & Changelog Tracking",
|
|
description:
|
|
"An automation crawls a rival's product, changelog, and careers pages and briefs you on what shipped.",
|
|
src: null,
|
|
demo: {
|
|
prompt:
|
|
"Every Monday, crawl our competitors' changelogs and brief me on what they shipped.",
|
|
steps: [
|
|
{
|
|
title: "Web Crawler",
|
|
items: [
|
|
"competitor-a.com/changelog · 6 entries",
|
|
"competitor-b.com/whats-new · 3 entries",
|
|
],
|
|
},
|
|
{
|
|
title: "Create automation",
|
|
items: ["Weekly changelog brief · Mondays 8:00"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "Competitor A shipped SSO + audit logs",
|
|
secondary: "changelog · Jun 30 · enterprise push",
|
|
},
|
|
{
|
|
primary: "Competitor B launched API v2 beta",
|
|
secondary: "whats-new · Jul 2 · targets developers",
|
|
},
|
|
],
|
|
summary: "Brief saved to workspace · automation runs Mondays 8:00",
|
|
},
|
|
},
|
|
{
|
|
id: "serp-watch",
|
|
title: "Rank & Ad Monitoring",
|
|
description:
|
|
"Automations track the Google rankings, paid ads, and AI Overview citations your market actually sees.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Track who ranks and runs ads for our top 10 keywords in the US.",
|
|
steps: [
|
|
{
|
|
title: "Google Search",
|
|
items: ["Scraping 10 SERPs (US) · organic, ads, AI Overviews"],
|
|
},
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Diff against last capture", "Flag rank and ad movements"],
|
|
},
|
|
{
|
|
title: "Create automation",
|
|
items: ["Daily rank + ad watch on these keywords"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: '"competitive intelligence tools" — you #4, ↓1',
|
|
secondary: "Competitor A took #3 · runs 2 sponsored ads",
|
|
},
|
|
{
|
|
primary: "AI Overview cites Competitor B",
|
|
secondary: 'triggered on "brand monitoring software"',
|
|
},
|
|
],
|
|
summary: "10 SERPs captured · 3 movements flagged · daily automation on",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "lead-generation",
|
|
label: "B2B Lead Generation",
|
|
useCases: [
|
|
{
|
|
id: "local-leads",
|
|
title: "Local Business Leads",
|
|
description:
|
|
"Turn a category and a territory into a lead list with phones, websites, ratings, and decision-maker contacts.",
|
|
src: null,
|
|
demo: {
|
|
prompt:
|
|
"Find the top 5 burger places in San Jose and pull staff contacts from their websites.",
|
|
steps: [
|
|
{
|
|
title: "Google Maps",
|
|
items: ['Searching "burger san jose" · top 5 by rating and reviews'],
|
|
},
|
|
{
|
|
title: "Web Crawler",
|
|
items: ["Visiting 5 business sites", "Extracting staff and contact pages"],
|
|
},
|
|
{
|
|
title: "Save document",
|
|
items: ["Lead list with phones, sites, contacts"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "The Counter — 4.5★ (2,310 reviews)",
|
|
secondary: "+1 408-423-9200 · thecounter.com · 2 contacts found",
|
|
},
|
|
{
|
|
primary: "Paper Plane — 4.6★ (1,882 reviews)",
|
|
secondary: "site crawled · owner + events email found",
|
|
},
|
|
{
|
|
primary: "Smoking Pig BBQ — 4.4★ (3,041 reviews)",
|
|
secondary: "+1 408-380-4784 · catering contact found",
|
|
},
|
|
],
|
|
summary: "5 places · 9 contacts with provenance · saved as lead list",
|
|
},
|
|
},
|
|
{
|
|
id: "team-rosters",
|
|
title: "Team Rosters & Contacts",
|
|
description:
|
|
"Spider any company site and pull the full team with emails, socials, and provenance, exported to CSV.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Get the complete a16z team roster and save it as a CSV in my workspace.",
|
|
steps: [
|
|
{
|
|
title: "Web Crawler",
|
|
items: ["a16z.com/team · following profile links", "142 profiles found"],
|
|
},
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Enrich each profile with email or LinkedIn"],
|
|
},
|
|
{
|
|
title: "Write file",
|
|
items: ["a16z-team.csv · saved to workspace"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "142 team profiles extracted",
|
|
secondary: "name, role, focus area, profile URL",
|
|
},
|
|
{
|
|
primary: "89 enriched with email or LinkedIn",
|
|
secondary: "pulled from bios and linked pages",
|
|
},
|
|
{ primary: "a16z-team.csv", secondary: "saved to your workspace · ready to export" },
|
|
],
|
|
summary: "Full roster in 94s · every row cites its source page",
|
|
},
|
|
},
|
|
{
|
|
id: "portfolio-mapping",
|
|
title: "Portfolio & Market Mapping",
|
|
description:
|
|
"Map an investor's portfolio or a whole category, then enrich every company with pricing and contacts.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Get the a16z team and their portfolio companies.",
|
|
steps: [
|
|
{
|
|
title: "Web Crawler",
|
|
items: ["a16z.com/team · 142 profiles", "a16z.com/portfolio · 312 companies"],
|
|
},
|
|
{
|
|
title: "Plan tasks",
|
|
items: [
|
|
"Match partners to their portfolio boards",
|
|
"Enrich your category with pricing",
|
|
],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "312 portfolio companies mapped",
|
|
secondary: "sector, stage, website, one-line pitch",
|
|
},
|
|
{
|
|
primary: "48 in your category",
|
|
secondary: "enriched with pricing page + team size",
|
|
},
|
|
],
|
|
summary: "Market map saved · new investments auto-added by automation",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "brand-listening",
|
|
label: "Brand & Market Listening",
|
|
useCases: [
|
|
{
|
|
id: "reddit-listening",
|
|
title: "Reddit Brand Monitoring",
|
|
description:
|
|
"Hear what your market says about you, your competitors, and your category in the threads where buyers speak candidly.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Give me 20 posts on Reddit where people ask for an alternative to NotebookLM.",
|
|
steps: [
|
|
{
|
|
title: "Reddit",
|
|
items: [
|
|
'Searching "notebooklm alternative" across Reddit',
|
|
"37 posts found · ranking by relevance and upvotes",
|
|
],
|
|
},
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Tag each post: buying intent, churn signal, question"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "Best NotebookLM alternative that's open source?",
|
|
secondary: "r/selfhosted · 214 upvotes · buying intent",
|
|
},
|
|
{
|
|
primary: "NotebookLM keeps losing my sources, what else?",
|
|
secondary: "r/artificial · 96 upvotes · churn signal",
|
|
},
|
|
{
|
|
primary: "Anything like NotebookLM but with an API?",
|
|
secondary: "r/LocalLLaMA · 71 upvotes · developer intent",
|
|
},
|
|
],
|
|
summary: "20 posts · 8 with buying intent · daily tracking automation on",
|
|
},
|
|
},
|
|
{
|
|
id: "youtube-sentiment",
|
|
title: "YouTube Audience Sentiment",
|
|
description:
|
|
"Pull videos, transcripts, and comments at scale to mine what audiences praise and complain about.",
|
|
src: null,
|
|
demo: {
|
|
prompt:
|
|
"Analyze the comments on our competitor's last 10 videos and cluster the complaints.",
|
|
steps: [
|
|
{
|
|
title: "Youtube",
|
|
items: ["Fetching last 10 videos", "4,812 comments pulled"],
|
|
},
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Cluster complaints by theme", "Score sentiment per cluster"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "Pricing complaints — 31% of negative comments",
|
|
secondary: '"went from free to $20/mo overnight"',
|
|
},
|
|
{
|
|
primary: "Export limits — 22%",
|
|
secondary: '"can\'t get my own data out"',
|
|
},
|
|
{
|
|
primary: "Praise: onboarding — 18% positive",
|
|
secondary: "worth copying in your messaging",
|
|
},
|
|
],
|
|
summary: "4,812 comments clustered · sentiment report saved",
|
|
},
|
|
},
|
|
{
|
|
id: "switcher-mining",
|
|
title: "Switcher & Intent Mining",
|
|
description:
|
|
"Find the people actively looking for an alternative to a competitor, ranked by how ready they are to move.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Find people asking for alternatives to our biggest competitor this month.",
|
|
steps: [
|
|
{
|
|
title: "Reddit",
|
|
items: [
|
|
'Searching "alternative" mentions · past month',
|
|
"12 active switcher threads",
|
|
],
|
|
},
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Rank by recency and engagement", "Extract switching triggers"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "12 threads with active switchers",
|
|
secondary: "ranked by recency and engagement",
|
|
},
|
|
{
|
|
primary: "Top trigger: API price increase",
|
|
secondary: "mentioned in 7 of 12 threads",
|
|
},
|
|
],
|
|
summary: "Outreach-ready summaries drafted for the 5 hottest threads",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "market-research",
|
|
label: "Market Research",
|
|
useCases: [
|
|
{
|
|
id: "deep-research",
|
|
title: "Deep Research on the Live Web",
|
|
description:
|
|
"The agent crawls dozens of live sources on a question and synthesizes a cited answer, not a stale index.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Research the AI note-taking market and build a landscape brief with citations.",
|
|
steps: [
|
|
{
|
|
title: "Research",
|
|
items: ["Crawling 38 live sources", "Vendor sites, reviews, pricing pages"],
|
|
},
|
|
{
|
|
title: "Generate report",
|
|
items: ["Landscape brief · 24 inline citations"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "23 vendors mapped across 4 segments",
|
|
secondary: "consumer, prosumer, team, developer-first",
|
|
},
|
|
{
|
|
primary: "Pricing clusters at $10 and $20/mo",
|
|
secondary: "3 vendors moved upmarket this quarter",
|
|
},
|
|
],
|
|
summary: "Landscape brief saved · 24 inline citations you can check",
|
|
},
|
|
},
|
|
{
|
|
id: "geo-monitoring",
|
|
title: "AI Overview & GEO Tracking",
|
|
description:
|
|
"Capture when Google's AI Overviews answer your market's queries, and exactly which sources they cite.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Which of our target keywords trigger an AI Overview, and who gets cited?",
|
|
steps: [
|
|
{
|
|
title: "Google Search",
|
|
items: ["Scraping 25 SERPs", "Capturing AI Overviews and citations"],
|
|
},
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Map citations to competitors", "Compute your citation gap"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "9 of 25 keywords trigger an AI Overview",
|
|
secondary: "up from 6 last month",
|
|
},
|
|
{
|
|
primary: "Competitor A cited on 4 · you on 1",
|
|
secondary: "their listicle wins 3 of those citations",
|
|
},
|
|
],
|
|
summary: "Citation gap report saved · weekly re-check scheduled",
|
|
},
|
|
},
|
|
{
|
|
id: "cited-briefs",
|
|
title: "Cited Briefs & Alerts",
|
|
description:
|
|
"Everything the agents gather lands in your workspace as briefs and alerts with sources you can check.",
|
|
src: null,
|
|
demo: {
|
|
prompt: "Send me a Monday brief of every competitor change detected last week.",
|
|
steps: [
|
|
{
|
|
title: "Plan tasks",
|
|
items: ["Collect pricing, changelog, SERP, Reddit signals"],
|
|
},
|
|
{
|
|
title: "Create automation",
|
|
items: ["Weekly brief · Mondays 8:00 · workspace + email"],
|
|
},
|
|
],
|
|
rows: [
|
|
{
|
|
primary: "Sources: pricing, changelogs, SERPs, Reddit",
|
|
secondary: "everything your agents tracked this week",
|
|
},
|
|
{
|
|
primary: "Delivered to workspace + email",
|
|
secondary: "every claim links to its source",
|
|
},
|
|
],
|
|
summary: "Automation created · first brief lands Monday 8:00",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "platform-agent",
|
|
label: "SurfSense Agent",
|
|
useCases: [
|
|
{
|
|
id: "chat-workspace",
|
|
title: "Chat With Everything You Gather",
|
|
description:
|
|
"Ask questions across every crawl, mention, and document in your workspace and get answers with inline citations.",
|
|
src: `${HERO_TUTORIAL}/BQnaGif_compressed.mp4`,
|
|
},
|
|
{
|
|
id: "report",
|
|
title: "AI Report Generator",
|
|
description:
|
|
"Turn your intelligence into cited research reports, then export to PDF or Markdown.",
|
|
src: `${HERO_TUTORIAL}/ReportGenGif_compressed.mp4`,
|
|
},
|
|
{
|
|
id: "podcast",
|
|
title: "AI Podcast Generator",
|
|
description: "Turn any brief or folder into a two-host AI podcast in under 20 seconds.",
|
|
src: `${HERO_TUTORIAL}/PodcastGenGif.mp4`,
|
|
},
|
|
{
|
|
id: "presentation",
|
|
title: "AI Presentation & Video Maker",
|
|
description: "Create editable slide decks and narrated video overviews from your findings.",
|
|
src: `${HERO_TUTORIAL}/video_gen_surf.mp4`,
|
|
},
|
|
{
|
|
id: "connect",
|
|
title: "Build Your Knowledge Base",
|
|
description:
|
|
"Upload files or sync Google Drive, OneDrive, and Dropbox into one searchable knowledge base alongside everything your agents gather.",
|
|
src: `${HERO_TUTORIAL}/ConnectorFlowGif.mp4`,
|
|
},
|
|
{
|
|
id: "collab",
|
|
title: "Collaborative AI Chat",
|
|
description: "Work on AI conversations with your team in real time.",
|
|
src: `${HERO_REALTIME}/RealTimeChatGif.mp4`,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export function HeroSection() {
|
|
return (
|
|
<div className="mx-auto w-full max-w-7xl min-w-0 pt-36">
|
|
<div className="mt-4 flex w-full min-w-0 flex-col items-start px-2 md:px-8 xl:px-0">
|
|
<h1
|
|
className={cn(
|
|
"relative mt-4 max-w-4xl text-left text-4xl font-bold tracking-tight text-balance text-neutral-900 sm:text-5xl md:text-6xl dark:text-neutral-50"
|
|
)}
|
|
>
|
|
<Balancer>Give your AI agents competitive intelligence.</Balancer>
|
|
</h1>
|
|
<div className="mt-4 flex w-full flex-col items-start justify-between gap-4 md:mt-8 md:flex-row md:items-end md:gap-10">
|
|
<div>
|
|
<p
|
|
className={cn(
|
|
"relative mb-8 max-w-2xl text-left text-sm text-neutral-600 antialiased sm:text-base md:text-lg dark:text-neutral-400"
|
|
)}
|
|
>
|
|
SurfSense is an open-source competitive intelligence platform. Your AI agents monitor
|
|
competitors, track rankings, and listen to your market with live data from the
|
|
platforms that matter, through one API or MCP server.
|
|
</p>
|
|
|
|
<div className="relative mb-4 flex w-full flex-col justify-center gap-y-2 sm:flex-row sm:justify-start sm:space-y-0 sm:space-x-4">
|
|
<GetStartedButton />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<BrowserWindow />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GetStartedButton() {
|
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
|
|
|
const handleGoogleLogin = () => {
|
|
if (isRedirecting) return;
|
|
setIsRedirecting(true);
|
|
trackLoginAttempt("google");
|
|
window.location.href = buildBackendUrl("/auth/google/authorize-redirect");
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={handleGoogleLogin}
|
|
disabled={isRedirecting}
|
|
className="runtime-auth-google h-14 w-full cursor-pointer gap-3 rounded-lg border border-white bg-white text-center text-base font-medium text-[#1f1f1f] shadow-sm transition duration-150 hover:bg-zinc-100 hover:text-[#1f1f1f] sm:w-56 dark:border-white"
|
|
>
|
|
<GoogleLogo className="h-5 w-5" />
|
|
<span>Continue with Google</span>
|
|
</Button>
|
|
<Button
|
|
asChild
|
|
variant="ghost"
|
|
className="runtime-auth-local h-14 w-full rounded-lg bg-black text-center text-base font-medium text-white shadow-sm ring-1 shadow-black/10 ring-black/10 transition duration-150 active:scale-98 hover:bg-black sm:w-52 dark:bg-white dark:text-black dark:hover:bg-white"
|
|
>
|
|
<Link href="/login">Get Started</Link>
|
|
</Button>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const TabVideo = memo(function TabVideo({
|
|
src,
|
|
title,
|
|
reduceMotion,
|
|
}: {
|
|
src: string;
|
|
title: string;
|
|
reduceMotion: boolean;
|
|
}) {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const [hasLoaded, setHasLoaded] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setHasLoaded(false);
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
video.currentTime = 0;
|
|
// Respect reduced-motion: show the first frame and expose controls instead of autoplaying.
|
|
if (!reduceMotion) {
|
|
video.play().catch(() => {});
|
|
}
|
|
}, [reduceMotion]);
|
|
|
|
const handleCanPlay = useCallback(() => {
|
|
setHasLoaded(true);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<video
|
|
ref={videoRef}
|
|
key={src}
|
|
src={src}
|
|
preload={reduceMotion ? "metadata" : "auto"}
|
|
aria-label={`${title} demo`}
|
|
autoPlay={!reduceMotion}
|
|
controls={reduceMotion}
|
|
loop
|
|
muted
|
|
playsInline
|
|
onCanPlay={handleCanPlay}
|
|
className="aspect-video w-full rounded-lg sm:rounded-xl"
|
|
/>
|
|
{!hasLoaded && (
|
|
<Skeleton className="absolute inset-0 aspect-video w-full rounded-lg bg-neutral-100 motion-reduce:animate-none sm:rounded-xl dark:bg-neutral-800" />
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const UseCasePane = memo(function UseCasePane({
|
|
useCase,
|
|
reduceMotion,
|
|
}: {
|
|
useCase: HeroUseCase;
|
|
reduceMotion: boolean;
|
|
}) {
|
|
const { expanded, open, close } = useExpandedMedia();
|
|
const hasVideo = Boolean(useCase.src);
|
|
|
|
const media = hasVideo ? (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={open}
|
|
aria-label={`Expand ${useCase.title} demo`}
|
|
className="h-auto w-full cursor-pointer rounded-none bg-neutral-50 p-2 hover:bg-neutral-50 sm:p-3 dark:bg-neutral-950 dark:hover:bg-neutral-950"
|
|
>
|
|
<TabVideo src={useCase.src as string} title={useCase.title} reduceMotion={reduceMotion} />
|
|
</Button>
|
|
) : (
|
|
<div className="bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950">
|
|
{useCase.demo && <HeroChatDemo demo={useCase.demo} reduceMotion={reduceMotion} />}
|
|
</div>
|
|
);
|
|
|
|
const card = (
|
|
<div className="relative overflow-hidden rounded-tl-xl rounded-tr-xl bg-white shadow-sm ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-950">
|
|
<div className="flex items-center gap-3 border-b border-neutral-200/60 px-4 py-3 sm:px-6 sm:py-4 dark:border-neutral-700/60">
|
|
<div className="min-w-0">
|
|
<h3 className="truncate text-base font-semibold text-neutral-900 sm:text-lg dark:text-white">
|
|
{useCase.title}
|
|
</h3>
|
|
<p className="text-sm text-neutral-500 text-pretty dark:text-neutral-400">
|
|
{useCase.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{media}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{reduceMotion ? (
|
|
card
|
|
) : (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.99, filter: "blur(10px)" }}
|
|
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
className="will-change-transform"
|
|
>
|
|
{card}
|
|
</motion.div>
|
|
)}
|
|
|
|
<AnimatePresence>
|
|
{expanded && hasVideo && (
|
|
<ExpandedMediaOverlay
|
|
src={useCase.src as string}
|
|
alt={`${useCase.title} demo`}
|
|
onClose={close}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
});
|
|
|
|
const CategoryPanel = memo(function CategoryPanel({
|
|
category,
|
|
reduceMotion,
|
|
}: {
|
|
category: HeroCategory;
|
|
reduceMotion: boolean;
|
|
}) {
|
|
return (
|
|
<div className="flex w-full flex-col gap-3">
|
|
<Tabs
|
|
defaultValue={category.useCases[0]?.id}
|
|
orientation="vertical"
|
|
className="flex w-full flex-col gap-3 md:flex-row md:gap-4"
|
|
>
|
|
<ScrollArea className="w-full md:w-56 md:shrink-0">
|
|
<TabsList className="flex h-auto w-max gap-1 bg-transparent p-0 md:w-full md:flex-col md:items-stretch">
|
|
{category.useCases.map((useCase) => (
|
|
<TabsTrigger
|
|
key={useCase.id}
|
|
value={useCase.id}
|
|
className="h-auto shrink-0 touch-manipulation justify-start rounded-md px-3 py-2 text-left text-xs whitespace-normal data-[state=active]:bg-background data-[state=active]:shadow-sm sm:text-sm md:w-full"
|
|
>
|
|
{useCase.title}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
<ScrollBar orientation="horizontal" className="md:hidden" />
|
|
</ScrollArea>
|
|
<div className="min-w-0 flex-1">
|
|
{category.useCases.map((useCase) => (
|
|
<TabsContent key={useCase.id} value={useCase.id} className="mt-0">
|
|
<UseCasePane useCase={useCase} reduceMotion={reduceMotion} />
|
|
</TabsContent>
|
|
))}
|
|
</div>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const BrowserWindow = () => {
|
|
const [activeCategory, setActiveCategory] = useState(CATEGORIES[0].id);
|
|
const reduceMotion = useReducedMotion() ?? false;
|
|
|
|
return (
|
|
<Tabs
|
|
value={activeCategory}
|
|
onValueChange={setActiveCategory}
|
|
className="relative my-4 flex w-full flex-col items-start justify-start gap-0 overflow-hidden rounded-2xl shadow-2xl md:my-12"
|
|
>
|
|
<div className="flex w-full items-center justify-start overflow-hidden bg-gray-200 py-4 pl-4 dark:bg-neutral-800">
|
|
<div className="mr-6 flex items-center gap-2">
|
|
<div className="size-3 rounded-full bg-red-500" />
|
|
<div className="size-3 rounded-full bg-yellow-500" />
|
|
<div className="size-3 rounded-full bg-green-500" />
|
|
</div>
|
|
<ScrollArea className="min-w-0 flex-1">
|
|
<TabsList className="flex h-auto w-max items-center gap-1 bg-transparent p-0 pr-4">
|
|
{CATEGORIES.map((category, index) => (
|
|
<React.Fragment key={category.id}>
|
|
<TabsTrigger
|
|
value={category.id}
|
|
className="h-auto shrink-0 touch-manipulation gap-1.5 rounded-md px-2.5 py-1 text-xs data-[state=active]:bg-background data-[state=active]:shadow sm:text-sm"
|
|
>
|
|
{category.label}
|
|
</TabsTrigger>
|
|
{index !== CATEGORIES.length - 1 && (
|
|
<Separator
|
|
orientation="vertical"
|
|
className="h-4 bg-neutral-300 dark:bg-neutral-700"
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</TabsList>
|
|
<ScrollBar orientation="horizontal" />
|
|
</ScrollArea>
|
|
</div>
|
|
<div className="w-full overflow-hidden bg-gray-100/50 px-4 pt-4 dark:bg-neutral-950">
|
|
{CATEGORIES.map((category) => (
|
|
<TabsContent key={category.id} value={category.id} className="mt-0">
|
|
<CategoryPanel category={category} reduceMotion={reduceMotion} />
|
|
</TabsContent>
|
|
))}
|
|
</div>
|
|
</Tabs>
|
|
);
|
|
};
|