'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
)}
>
)
}