'use client' import { useState, useEffect } from 'react' import { formatNumber, Modal } from '@ciphera-net/ui' 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 { title: string description: string items: FrustrationElement[] total: number showAvgClicks?: boolean loading: boolean fetchAll?: () => Promise<{ items: FrustrationElement[]; total: number }> } function SkeletonRows() { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
) } 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 ( ) } const GRID_WITH_AVG = 'grid grid-cols-[1fr_60px_50px_64px_64px_40px] items-center gap-2 h-9 px-2 -mx-2' const GRID_NO_AVG = 'grid grid-cols-[1fr_60px_64px_64px_40px] items-center gap-2 h-9 px-2 -mx-2' function Row({ item, showAvgClicks, }: { item: FrustrationElement showAvgClicks?: boolean }) { return (
{item.page_path}
{showAvgClicks && ( {item.avg_click_count != null ? item.avg_click_count.toFixed(1) : '–'} )} {item.sessions} {formatRelativeTime(item.last_seen)} {formatNumber(item.count)}
) } export default function FrustrationTable({ title, description, items, total, showAvgClicks, loading, fetchAll, }: FrustrationTableProps) { const [isModalOpen, setIsModalOpen] = useState(false) const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) const hasData = items.length > 0 const showViewAll = hasData && total > items.length useEffect(() => { if (isModalOpen && fetchAll) { const load = async () => { setIsLoadingFull(true) try { const result = await fetchAll() setFullData(result.items) } catch { // silent } finally { setIsLoadingFull(false) } } load() } else { setFullData([]) } }, [isModalOpen, fetchAll]) return ( <>

{title}

{showViewAll && ( )}

{description}

{loading ? ( ) : hasData ? (
{/* Column headers */}
Selector / Page {showAvgClicks && Avg} Sessions Last Seen Count
{items.map((item, i) => ( ))}
) : (

No {title.toLowerCase()} detected

{description}. Data will appear here once frustration signals are detected on your site.

)}
setIsModalOpen(false)} title={title} className="max-w-2xl" >
{isLoadingFull ? (
) : fullData.length > 0 ? (
{fullData.map((item, i) => ( ))}
) : (

No data available

)}
) }