public: custom homepage tracking
This commit is contained in:
@@ -6,6 +6,7 @@ import { getRootMetadata } from '@/lib/metadata';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import './global.css';
|
import './global.css';
|
||||||
import { OpenPanelComponent } from '@openpanel/nextjs';
|
import { OpenPanelComponent } from '@openpanel/nextjs';
|
||||||
|
import { ScrollTracker } from '@/components/scroll-tracker';
|
||||||
|
|
||||||
const font = Geist({
|
const font = Geist({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -39,6 +40,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<RootProvider>
|
<RootProvider>
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
</RootProvider>
|
</RootProvider>
|
||||||
|
<ScrollTracker />
|
||||||
{process.env.NEXT_PUBLIC_OP_CLIENT_ID && (
|
{process.env.NEXT_PUBLIC_OP_CLIENT_ID && (
|
||||||
<OpenPanelComponent
|
<OpenPanelComponent
|
||||||
clientId={process.env.NEXT_PUBLIC_OP_CLIENT_ID}
|
clientId={process.env.NEXT_PUBLIC_OP_CLIENT_ID}
|
||||||
|
|||||||
43
apps/public/src/components/feature-card-hover-track.tsx
Normal file
43
apps/public/src/components/feature-card-hover-track.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useOpenPanel } from '@openpanel/nextjs';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
interface FeatureCardHoverTrackProps {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeatureCardHoverTrack({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: FeatureCardHoverTrackProps) {
|
||||||
|
const { track } = useOpenPanel();
|
||||||
|
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | 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
|
||||||
|
<div
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { FeatureCardHoverTrack } from '@/components/feature-card-hover-track';
|
||||||
|
|
||||||
interface FeatureCardProps {
|
interface FeatureCardProps {
|
||||||
link?: {
|
link?: {
|
||||||
@@ -64,6 +66,7 @@ export function FeatureCard({
|
|||||||
}: FeatureCardProps) {
|
}: FeatureCardProps) {
|
||||||
if (illustration) {
|
if (illustration) {
|
||||||
return (
|
return (
|
||||||
|
<FeatureCardHoverTrack title={title}>
|
||||||
<FeatureCardContainer className={className}>
|
<FeatureCardContainer className={className}>
|
||||||
{illustration}
|
{illustration}
|
||||||
<div className="col gap-2" data-content>
|
<div className="col gap-2" data-content>
|
||||||
@@ -80,10 +83,12 @@ export function FeatureCard({
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</FeatureCardContainer>
|
</FeatureCardContainer>
|
||||||
|
</FeatureCardHoverTrack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<FeatureCardHoverTrack title={title}>
|
||||||
<FeatureCardContainer className={className}>
|
<FeatureCardContainer className={className}>
|
||||||
{Icon && <Icon className="size-6" />}
|
{Icon && <Icon className="size-6" />}
|
||||||
<div className="col gap-2">
|
<div className="col gap-2">
|
||||||
@@ -100,5 +105,6 @@ export function FeatureCard({
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</FeatureCardContainer>
|
</FeatureCardContainer>
|
||||||
|
</FeatureCardHoverTrack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
37
apps/public/src/components/scroll-tracker.tsx
Normal file
37
apps/public/src/components/scroll-tracker.tsx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user