feat: polish behavior page UI with 8 improvements

- Add column headers to rage/dead click tables
- Rich empty states with icons matching dashboard pattern
- Add frustration trend comparison chart (current vs previous period)
- Show "New" badge instead of misleading "+100%" when previous period is 0
- Click-to-copy on CSS selectors with toast feedback
- Normalize min-height to 270px for consistent card sizing
- Fix page title to include site domain (Behavior · domain | Pulse)
- Add "last seen" column with relative timestamps
This commit is contained in:
Usman Baig
2026-03-12 18:03:22 +01:00
parent 585f37f444
commit 2f01be1c67
5 changed files with 256 additions and 29 deletions

View File

@@ -2,8 +2,10 @@
import { useState, useEffect } from 'react'
import { formatNumber, Modal } from '@ciphera-net/ui'
import { FrameCornersIcon } from '@phosphor-icons/react'
import { FrameCornersIcon, Copy, Check, CursorClick } from '@phosphor-icons/react'
import { toast } from '@ciphera-net/ui'
import type { FrustrationElement } from '@/lib/api/stats'
import { formatRelativeTime } from '@/lib/utils/formatDate'
import { ListSkeleton } from '@/components/skeletons'
interface FrustrationTableProps {
@@ -32,6 +34,37 @@ function SkeletonRows() {
)
}
function SelectorCell({ selector }: { selector: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = (e: React.MouseEvent) => {
e.stopPropagation()
navigator.clipboard.writeText(selector)
setCopied(true)
toast.success('Selector copied')
setTimeout(() => setCopied(false), 2000)
}
return (
<button
onClick={handleCopy}
className="flex items-center gap-1 min-w-0 group/copy cursor-pointer"
title={selector}
>
<span className="text-sm font-mono text-neutral-900 dark:text-white truncate max-w-[180px]">
{selector}
</span>
<span className="opacity-0 group-hover/copy:opacity-100 transition-opacity shrink-0">
{copied ? (
<Check className="w-3 h-3 text-green-500" />
) : (
<Copy className="w-3 h-3 text-neutral-400" />
)}
</span>
</button>
)
}
function Row({
item,
showAvgClicks,
@@ -42,14 +75,9 @@ function Row({
return (
<div className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex items-center gap-3 flex-1 min-w-0">
<SelectorCell selector={item.selector} />
<span
className="text-sm font-mono text-neutral-900 dark:text-white truncate max-w-[200px]"
title={item.selector}
>
{item.selector}
</span>
<span
className="text-xs text-neutral-400 dark:text-neutral-500 truncate max-w-[140px]"
className="text-xs text-neutral-400 dark:text-neutral-500 truncate max-w-[120px]"
title={item.page_path}
>
{item.page_path}
@@ -64,6 +92,9 @@ function Row({
<span className="text-xs text-neutral-400 dark:text-neutral-500 tabular-nums">
{item.sessions} {item.sessions === 1 ? 'session' : 'sessions'}
</span>
<span className="text-xs text-neutral-400 dark:text-neutral-500 tabular-nums" title={item.last_seen}>
{formatRelativeTime(item.last_seen)}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 tabular-nums">
{formatNumber(item.count)}
</span>
@@ -129,19 +160,40 @@ export default function FrustrationTable({
{description}
</p>
<div className="flex-1 min-h-[200px]">
<div className="flex-1 min-h-[270px]">
{loading ? (
<SkeletonRows />
) : hasData ? (
<div className="space-y-0.5">
{items.map((item, i) => (
<Row key={`${item.selector}-${item.page_path}-${i}`} item={item} showAvgClicks={showAvgClicks} />
))}
<div>
{/* Column headers */}
<div className="flex items-center justify-between px-2 -mx-2 mb-2 text-xs font-medium text-neutral-400 dark:text-neutral-500 uppercase tracking-wider">
<div className="flex items-center gap-3">
<span>Selector</span>
<span>Page</span>
</div>
<div className="flex items-center gap-4">
{showAvgClicks && <span>Avg</span>}
<span>Sessions</span>
<span>Last Seen</span>
<span>Count</span>
</div>
</div>
<div className="space-y-0.5">
{items.map((item, i) => (
<Row key={`${item.selector}-${item.page_path}-${i}`} item={item} showAvgClicks={showAvgClicks} />
))}
</div>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
No {title.toLowerCase()} detected in this period
<div className="h-full 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">
<CursorClick className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
No {title.toLowerCase()} detected
</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md">
{description}. Data will appear here once frustration signals are detected on your site.
</p>
</div>
)}