diff --git a/apps/public/src/app/layout.tsx b/apps/public/src/app/layout.tsx index 124aa833..ab1bf057 100644 --- a/apps/public/src/app/layout.tsx +++ b/apps/public/src/app/layout.tsx @@ -6,6 +6,7 @@ import { getRootMetadata } from '@/lib/metadata'; import { cn } from '@/lib/utils'; import './global.css'; import { OpenPanelComponent } from '@openpanel/nextjs'; +import { ScrollTracker } from '@/components/scroll-tracker'; const font = Geist({ subsets: ['latin'], @@ -39,6 +40,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { {children} + {process.env.NEXT_PUBLIC_OP_CLIENT_ID && ( | null>(null); + + const handleMouseEnter = () => { + hoverTimerRef.current = setTimeout(() => { + track('feature_card_hover', { title }); + hoverTimerRef.current = null; + }, 1500); + }; + + const handleMouseLeave = () => { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = null; + } + }; + + return ( + // Hover handlers for analytics only; no keyboard interaction needed + // biome-ignore lint/a11y/noNoninteractiveElementInteractions: analytics hover tracking +
+ {children} +
+ ); +} diff --git a/apps/public/src/components/feature-card.tsx b/apps/public/src/components/feature-card.tsx index c4b8351a..bf5be5fc 100644 --- a/apps/public/src/components/feature-card.tsx +++ b/apps/public/src/components/feature-card.tsx @@ -1,6 +1,8 @@ + import type { LucideIcon } from 'lucide-react'; import Link from 'next/link'; import { cn } from '@/lib/utils'; +import { FeatureCardHoverTrack } from '@/components/feature-card-hover-track'; interface FeatureCardProps { link?: { @@ -64,41 +66,45 @@ export function FeatureCard({ }: FeatureCardProps) { if (illustration) { return ( + + + {illustration} +
+

{title}

+

{description}

+
+ {children} + {link && ( + + {link.children} + + )} +
+
+ ); + } + + return ( + - {illustration} -
-

{title}

-

{description}

+ {Icon && } +
+

{title}

+

{description}

{children} {link && ( {link.children} )} - ); - } - - return ( - - {Icon && } -
-

{title}

-

{description}

-
- {children} - {link && ( - - {link.children} - - )} -
+ ); } diff --git a/apps/public/src/components/scroll-tracker.tsx b/apps/public/src/components/scroll-tracker.tsx new file mode 100644 index 00000000..0238b2a2 --- /dev/null +++ b/apps/public/src/components/scroll-tracker.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useOpenPanel } from '@openpanel/nextjs'; +import { usePathname } from 'next/navigation'; +import { useEffect, useRef } from 'react'; + +export function ScrollTracker() { + const { track } = useOpenPanel(); + const pathname = usePathname(); + const hasFired = useRef(false); + + useEffect(() => { + hasFired.current = false; + + const handleScroll = () => { + if (hasFired.current) { + return; + } + + const scrollTop = window.scrollY; + const docHeight = + document.documentElement.scrollHeight - window.innerHeight; + const percent = + docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; + + if (percent >= 50) { + hasFired.current = true; + track('scroll_half_way', { percent: Math.round(percent) }); + } + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, [track, pathname]); + + return null; +}