From d5aafdc48a736c3a508523832c4ba9b37819c9d3 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Thu, 12 Mar 2026 16:56:00 +0100 Subject: [PATCH] feat: add behavior page shell --- app/sites/[id]/behavior/page.tsx | 204 +++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 app/sites/[id]/behavior/page.tsx diff --git a/app/sites/[id]/behavior/page.tsx b/app/sites/[id]/behavior/page.tsx new file mode 100644 index 0000000..f96b3cd --- /dev/null +++ b/app/sites/[id]/behavior/page.tsx @@ -0,0 +1,204 @@ +'use client' + +import { useCallback, useEffect, useRef, useState } from 'react' +import { useParams } from 'next/navigation' +import { getDateRange, formatDate } from '@ciphera-net/ui' +import { Select, DatePicker } from '@ciphera-net/ui' +import { toast } from '@ciphera-net/ui' +import dynamic from 'next/dynamic' +import { + getFrustrationSummary, + getRageClicks, + getDeadClicks, + getFrustrationByPage, + type FrustrationSummary, + type FrustrationElement, + type FrustrationByPage, +} from '@/lib/api/stats' +import FrustrationSummaryCards from '@/components/behavior/FrustrationSummaryCards' +import FrustrationTable from '@/components/behavior/FrustrationTable' +import FrustrationByPageTable from '@/components/behavior/FrustrationByPageTable' +import { useDashboard } from '@/lib/swr/dashboard' + +const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth')) + +const TABLE_LIMIT = 7 + +function getThisWeekRange(): { start: string; end: string } { + const today = new Date() + const dayOfWeek = today.getDay() + const monday = new Date(today) + monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1)) + return { start: formatDate(monday), end: formatDate(today) } +} + +function getThisMonthRange(): { start: string; end: string } { + const today = new Date() + const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1) + return { start: formatDate(firstOfMonth), end: formatDate(today) } +} + +export default function BehaviorPage() { + const params = useParams() + const siteId = params.id as string + + const [period, setPeriod] = useState('30') + const [dateRange, setDateRange] = useState(() => getDateRange(30)) + const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) + + // Frustration data + const [summary, setSummary] = useState(null) + const [rageClicks, setRageClicks] = useState<{ items: FrustrationElement[]; total: number }>({ items: [], total: 0 }) + const [deadClicks, setDeadClicks] = useState<{ items: FrustrationElement[]; total: number }>({ items: [], total: 0 }) + const [byPage, setByPage] = useState([]) + const [loading, setLoading] = useState(true) + const refreshRef = useRef | null>(null) + + // Fetch dashboard data for scroll depth (goal_counts + stats) + const { data: dashboard } = useDashboard(siteId, dateRange.start, dateRange.end) + + const fetchData = useCallback(async () => { + try { + const [summaryData, rageData, deadData, pageData] = await Promise.all([ + getFrustrationSummary(siteId, dateRange.start, dateRange.end), + getRageClicks(siteId, dateRange.start, dateRange.end, TABLE_LIMIT), + getDeadClicks(siteId, dateRange.start, dateRange.end, TABLE_LIMIT), + getFrustrationByPage(siteId, dateRange.start, dateRange.end), + ]) + setSummary(summaryData) + setRageClicks(rageData) + setDeadClicks(deadData) + setByPage(pageData) + } catch { + toast.error('Failed to load behavior data') + } finally { + setLoading(false) + } + }, [siteId, dateRange.start, dateRange.end]) + + // Fetch on mount and when date range changes + useEffect(() => { + setLoading(true) + fetchData() + }, [fetchData]) + + // 60-second refresh interval + useEffect(() => { + refreshRef.current = setInterval(fetchData, 60_000) + return () => { + if (refreshRef.current) clearInterval(refreshRef.current) + } + }, [fetchData]) + + useEffect(() => { + document.title = 'Behavior | Pulse' + }, []) + + const fetchAllRage = useCallback( + () => getRageClicks(siteId, dateRange.start, dateRange.end, 100), + [siteId, dateRange.start, dateRange.end] + ) + + const fetchAllDead = useCallback( + () => getDeadClicks(siteId, dateRange.start, dateRange.end, 100), + [siteId, dateRange.start, dateRange.end] + ) + + return ( +
+ {/* Header */} +
+
+

+ Behavior +

+

+ Frustration signals and user engagement patterns +

+
+