public: custom homepage tracking

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-02-26 20:00:00 +01:00
parent baedf4343b
commit 4b150dd987
4 changed files with 113 additions and 25 deletions

View File

@@ -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}

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

View File

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

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