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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user