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

@@ -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 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 (
<FeatureCardHoverTrack title={title}>
<FeatureCardContainer className={className}>
{illustration}
<div className="col gap-2" data-content>
<h3 className="font-semibold text-xl">{title}</h3>
<p className="text-muted-foreground">{description}</p>
</div>
{children}
{link && (
<Link
className="mx-6 text-muted-foreground text-sm transition-colors hover:text-primary"
href={link.href}
>
{link.children}
</Link>
)}
</FeatureCardContainer>
</FeatureCardHoverTrack>
);
}
return (
<FeatureCardHoverTrack title={title}>
<FeatureCardContainer className={className}>
{illustration}
<div className="col gap-2" data-content>
<h3 className="font-semibold text-xl">{title}</h3>
<p className="text-muted-foreground">{description}</p>
{Icon && <Icon className="size-6" />}
<div className="col gap-2">
<h3 className="font-semibold text-lg">{title}</h3>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
{children}
{link && (
<Link
className="mx-6 text-muted-foreground text-sm transition-colors hover:text-primary"
className="text-muted-foreground text-sm transition-colors hover:text-primary"
href={link.href}
>
{link.children}
</Link>
)}
</FeatureCardContainer>
);
}
return (
<FeatureCardContainer className={className}>
{Icon && <Icon className="size-6" />}
<div className="col gap-2">
<h3 className="font-semibold text-lg">{title}</h3>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
{children}
{link && (
<Link
className="text-muted-foreground text-sm transition-colors hover:text-primary"
href={link.href}
>
{link.children}
</Link>
)}
</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;
}