Files
pulse/components/dashboard/ScrollDepth.tsx
Usman Baig 8b1d196812 feat: add automatic 404 detection, scroll depth tracking, and scroll depth dashboard card
- 404 detection: checks document.title for "404" or "not found", fires custom event, SPA-aware
- Scroll depth: passive scroll listener fires events at 25/50/75/100% thresholds
- ScrollDepth dashboard card: progress bar visualization showing % of visitors reaching each threshold
- Scroll events filtered out of GoalStats to avoid duplication
- Both features on by default, opt-out via data-no-404 / data-no-scroll
2026-03-06 20:00:22 +01:00

81 lines
3.1 KiB
TypeScript

'use client'
import { formatNumber } from '@ciphera-net/ui'
import { BarChartIcon } from '@ciphera-net/ui'
import type { GoalCountStat } from '@/lib/api/stats'
interface ScrollDepthProps {
goalCounts: GoalCountStat[]
totalPageviews: number
}
const THRESHOLDS = [25, 50, 75, 100] as const
export default function ScrollDepth({ goalCounts, totalPageviews }: ScrollDepthProps) {
const scrollCounts = new Map<number, number>()
for (const row of goalCounts) {
const match = row.event_name.match(/^scroll_(\d+)$/)
if (match) {
scrollCounts.set(Number(match[1]), row.count)
}
}
const hasData = scrollCounts.size > 0 && totalPageviews > 0
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
Scroll Depth
</h3>
</div>
{hasData ? (
<div className="space-y-3 flex-1 min-h-[200px]">
{THRESHOLDS.map((threshold) => {
const count = scrollCounts.get(threshold) ?? 0
const pct = totalPageviews > 0 ? Math.round((count / totalPageviews) * 100) : 0
const barWidth = Math.max(pct, 2)
return (
<div key={threshold} className="space-y-1">
<div className="flex items-center justify-between text-sm">
<span className="font-medium text-neutral-900 dark:text-white">
{threshold}%
</span>
<div className="flex items-center gap-2">
<span className="text-neutral-500 dark:text-neutral-400 tabular-nums">
{formatNumber(count)}
</span>
<span className="font-semibold text-brand-orange tabular-nums w-12 text-right">
{pct}%
</span>
</div>
</div>
<div className="h-2 rounded-full bg-neutral-100 dark:bg-neutral-800 overflow-hidden">
<div
className="h-full rounded-full bg-brand-orange transition-all duration-500"
style={{ width: `${barWidth}%` }}
/>
</div>
</div>
)
})}
</div>
) : (
<div className="flex-1 min-h-[200px] flex flex-col items-center justify-center text-center px-6 py-8 gap-4">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<BarChartIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No scroll data yet
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md">
Scroll depth tracking is automatic data will appear here once visitors start scrolling on your pages.
</p>
</div>
)}
</div>
)
}