diff --git a/components/behavior/FrustrationTable.tsx b/components/behavior/FrustrationTable.tsx
new file mode 100644
index 0000000..13e1276
--- /dev/null
+++ b/components/behavior/FrustrationTable.tsx
@@ -0,0 +1,177 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { formatNumber, Modal } from '@ciphera-net/ui'
+import { FrameCornersIcon } from '@phosphor-icons/react'
+import type { FrustrationElement } from '@/lib/api/stats'
+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 Row({
+ item,
+ showAvgClicks,
+}: {
+ item: FrustrationElement
+ showAvgClicks?: boolean
+}) {
+ return (
+
+
+
+ {item.selector}
+
+
+ {item.page_path}
+
+
+
+ {showAvgClicks && item.avg_click_count != null && (
+
+ avg {item.avg_click_count.toFixed(1)}
+
+ )}
+
+ {item.sessions} {item.sessions === 1 ? 'session' : 'sessions'}
+
+
+ {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 ? (
+
+ {items.map((item, i) => (
+
+ ))}
+
+ ) : (
+
+
+ No {title.toLowerCase()} detected in this period
+
+
+ )}
+
+
+
+ setIsModalOpen(false)}
+ title={title}
+ className="max-w-2xl"
+ >
+
+ {isLoadingFull ? (
+
+
+
+ ) : fullData.length > 0 ? (
+
+ {fullData.map((item, i) => (
+
+ ))}
+
+ ) : (
+
+ No data available
+
+ )}
+
+
+ >
+ )
+}