SurfSense/surfsense_web/components/homepage/hero-section.tsx
DESKTOP-RTLN3BA\$punk 7af3f535fc feat(pricing): enhance pricing page with SEO metadata and structured data
- 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.
2026-07-05 20:45:23 -07:00

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>
);
};