Skip to content

Commit 4b150dd

Browse files
committed
public: custom homepage tracking
1 parent baedf43 commit 4b150dd

File tree

4 files changed

+113
-25
lines changed

4 files changed

+113
-25
lines changed

apps/public/src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getRootMetadata } from '@/lib/metadata';
66
import { cn } from '@/lib/utils';
77
import './global.css';
88
import { OpenPanelComponent } from '@openpanel/nextjs';
9+
import { ScrollTracker } from '@/components/scroll-tracker';
910

1011
const font = Geist({
1112
subsets: ['latin'],
@@ -39,6 +40,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
3940
<RootProvider>
4041
<TooltipProvider>{children}</TooltipProvider>
4142
</RootProvider>
43+
<ScrollTracker />
4244
{process.env.NEXT_PUBLIC_OP_CLIENT_ID && (
4345
<OpenPanelComponent
4446
clientId={process.env.NEXT_PUBLIC_OP_CLIENT_ID}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import { useOpenPanel } from '@openpanel/nextjs';
4+
import { useRef } from 'react';
5+
6+
interface FeatureCardHoverTrackProps {
7+
title: string;
8+
children: React.ReactNode;
9+
}
10+
11+
export function FeatureCardHoverTrack({
12+
title,
13+
children,
14+
}: FeatureCardHoverTrackProps) {
15+
const { track } = useOpenPanel();
16+
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
17+
18+
const handleMouseEnter = () => {
19+
hoverTimerRef.current = setTimeout(() => {
20+
track('feature_card_hover', { title });
21+
hoverTimerRef.current = null;
22+
}, 1500);
23+
};
24+
25+
const handleMouseLeave = () => {
26+
if (hoverTimerRef.current) {
27+
clearTimeout(hoverTimerRef.current);
28+
hoverTimerRef.current = null;
29+
}
30+
};
31+
32+
return (
33+
// Hover handlers for analytics only; no keyboard interaction needed
34+
// biome-ignore lint/a11y/noNoninteractiveElementInteractions: analytics hover tracking
35+
<div
36+
onMouseEnter={handleMouseEnter}
37+
onMouseLeave={handleMouseLeave}
38+
role="group"
39+
>
40+
{children}
41+
</div>
42+
);
43+
}

apps/public/src/components/feature-card.tsx

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
12
import type { LucideIcon } from 'lucide-react';
23
import Link from 'next/link';
34
import { cn } from '@/lib/utils';
5+
import { FeatureCardHoverTrack } from '@/components/feature-card-hover-track';
46

57
interface FeatureCardProps {
68
link?: {
@@ -64,41 +66,45 @@ export function FeatureCard({
6466
}: FeatureCardProps) {
6567
if (illustration) {
6668
return (
69+
<FeatureCardHoverTrack title={title}>
70+
<FeatureCardContainer className={className}>
71+
{illustration}
72+
<div className="col gap-2" data-content>
73+
<h3 className="font-semibold text-xl">{title}</h3>
74+
<p className="text-muted-foreground">{description}</p>
75+
</div>
76+
{children}
77+
{link && (
78+
<Link
79+
className="mx-6 text-muted-foreground text-sm transition-colors hover:text-primary"
80+
href={link.href}
81+
>
82+
{link.children}
83+
</Link>
84+
)}
85+
</FeatureCardContainer>
86+
</FeatureCardHoverTrack>
87+
);
88+
}
89+
90+
return (
91+
<FeatureCardHoverTrack title={title}>
6792
<FeatureCardContainer className={className}>
68-
{illustration}
69-
<div className="col gap-2" data-content>
70-
<h3 className="font-semibold text-xl">{title}</h3>
71-
<p className="text-muted-foreground">{description}</p>
93+
{Icon && <Icon className="size-6" />}
94+
<div className="col gap-2">
95+
<h3 className="font-semibold text-lg">{title}</h3>
96+
<p className="text-muted-foreground text-sm">{description}</p>
7297
</div>
7398
{children}
7499
{link && (
75100
<Link
76-
className="mx-6 text-muted-foreground text-sm transition-colors hover:text-primary"
101+
className="text-muted-foreground text-sm transition-colors hover:text-primary"
77102
href={link.href}
78103
>
79104
{link.children}
80105
</Link>
81106
)}
82107
</FeatureCardContainer>
83-
);
84-
}
85-
86-
return (
87-
<FeatureCardContainer className={className}>
88-
{Icon && <Icon className="size-6" />}
89-
<div className="col gap-2">
90-
<h3 className="font-semibold text-lg">{title}</h3>
91-
<p className="text-muted-foreground text-sm">{description}</p>
92-
</div>
93-
{children}
94-
{link && (
95-
<Link
96-
className="text-muted-foreground text-sm transition-colors hover:text-primary"
97-
href={link.href}
98-
>
99-
{link.children}
100-
</Link>
101-
)}
102-
</FeatureCardContainer>
108+
</FeatureCardHoverTrack>
103109
);
104110
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
3+
import { useOpenPanel } from '@openpanel/nextjs';
4+
import { usePathname } from 'next/navigation';
5+
import { useEffect, useRef } from 'react';
6+
7+
export function ScrollTracker() {
8+
const { track } = useOpenPanel();
9+
const pathname = usePathname();
10+
const hasFired = useRef(false);
11+
12+
useEffect(() => {
13+
hasFired.current = false;
14+
15+
const handleScroll = () => {
16+
if (hasFired.current) {
17+
return;
18+
}
19+
20+
const scrollTop = window.scrollY;
21+
const docHeight =
22+
document.documentElement.scrollHeight - window.innerHeight;
23+
const percent =
24+
docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
25+
26+
if (percent >= 50) {
27+
hasFired.current = true;
28+
track('scroll_half_way', { percent: Math.round(percent) });
29+
}
30+
};
31+
32+
window.addEventListener('scroll', handleScroll, { passive: true });
33+
return () => window.removeEventListener('scroll', handleScroll);
34+
}, [track, pathname]);
35+
36+
return null;
37+
}

0 commit comments

Comments
 (0)