Files
pulse/components/behavior/FrustrationSummaryCards.tsx
Usman Baig a3c1af7c95 fix: frontend consistency audit — 55 files cleaned up
Consistency fixes:
- Extract getThisWeekRange/getThisMonthRange to shared lib/utils/dateRanges.ts
  (removed 4 identical copy-pasted definitions)
- Add error boundaries for behavior, cdn, search, pagespeed pages
  (4 new error.tsx files — previously fell through to generic parent error)
- Add "View setup guide" CTA to empty states on journeys and behavior pages
  (previously showed text with no actionable button)
- Fix non-lazy useState initializer in funnel detail page
- Fix Bot & Spam settings header from text-xl to text-2xl (matches all other sections)
- Add useMinimumLoading to PageSpeed skeleton (consistent with all other pages)

Cleanup:
- Remove 438 redundant dark: class prefixes (app is dark-mode only)
  text-neutral-500 dark:text-neutral-400 → text-neutral-400 (206 occurrences)
  text-neutral-900 dark:text-white → text-white (232 occurrences)
- Remove dead @stripe/react-stripe-js and @stripe/stripe-js packages
  (billing migrated to Polar, no code imports Stripe)
- Remove duplicate motion package (framer-motion is the one actually used)
2026-03-23 19:50:16 +01:00

125 lines
4.2 KiB
TypeScript

'use client'
import type { FrustrationSummary } from '@/lib/api/stats'
interface FrustrationSummaryCardsProps {
data: FrustrationSummary | null
loading: boolean
}
function pctChange(current: number, previous: number): { type: 'pct'; value: number } | { type: 'new' } | null {
if (previous === 0 && current === 0) return null
if (previous === 0) return { type: 'new' }
return { type: 'pct', value: Math.round(((current - previous) / previous) * 100) }
}
function ChangeIndicator({ change }: { change: ReturnType<typeof pctChange> }) {
if (change === null) return null
if (change.type === 'new') {
return (
<span className="text-xs font-medium bg-brand-orange/10 text-brand-orange px-1.5 py-0.5 rounded">
New
</span>
)
}
const isUp = change.value > 0
const isDown = change.value < 0
return (
<span
className={`text-xs font-medium ${
isUp
? 'text-red-600 dark:text-red-400'
: isDown
? 'text-green-600 dark:text-green-400'
: 'text-neutral-400'
}`}
>
{isUp ? '+' : ''}{change.value}%
</span>
)
}
function SkeletonCard() {
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<div className="animate-pulse space-y-3">
<div className="h-4 w-24 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-8 w-16 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-3 w-32 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div>
</div>
)
}
export default function FrustrationSummaryCards({ data, loading }: FrustrationSummaryCardsProps) {
if (loading || !data) {
return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
)
}
const rageChange = pctChange(data.rage_clicks, data.prev_rage_clicks)
const deadChange = pctChange(data.dead_clicks, data.prev_dead_clicks)
const topPage = data.rage_top_page || data.dead_top_page
const totalSignals = data.rage_clicks + data.dead_clicks
return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
{/* Rage Clicks */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<p className="text-sm font-medium text-neutral-400 mb-1">
Rage Clicks
</p>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-white tabular-nums">
{data.rage_clicks.toLocaleString()}
</span>
<ChangeIndicator change={rageChange} />
</div>
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
{data.rage_unique_elements} unique elements
</p>
</div>
{/* Dead Clicks */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<p className="text-sm font-medium text-neutral-400 mb-1">
Dead Clicks
</p>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-white tabular-nums">
{data.dead_clicks.toLocaleString()}
</span>
<ChangeIndicator change={deadChange} />
</div>
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
{data.dead_unique_elements} unique elements
</p>
</div>
{/* Total Frustration Signals */}
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
<p className="text-sm font-medium text-neutral-400 mb-1">
Total Signals
</p>
<span className="text-2xl font-bold text-white tabular-nums">
{totalSignals.toLocaleString()}
</span>
{topPage ? (
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
Top page: {topPage}
</p>
) : (
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
No data in this period
</p>
)}
</div>
</div>
)
}