'use client' import { logger } from '@/lib/utils/logger' import { useCallback, useEffect, useState, useMemo } from 'react' import { useParams, useRouter, useSearchParams } from 'next/navigation' import { motion } from 'framer-motion' import { getPerformanceByPage, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getCampaigns, type Stats, type DailyStat, } from '@/lib/api/stats' import { getDateRange, formatDate } from '@ciphera-net/ui' import { toast } from '@ciphera-net/ui' import { Button } from '@ciphera-net/ui' import { Select, DatePicker, DownloadIcon } from '@ciphera-net/ui' import { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons' import ExportModal from '@/components/dashboard/ExportModal' import ContentStats from '@/components/dashboard/ContentStats' import TopReferrers from '@/components/dashboard/TopReferrers' import Locations from '@/components/dashboard/Locations' import TechSpecs from '@/components/dashboard/TechSpecs' import Chart from '@/components/dashboard/Chart' import PerformanceStats from '@/components/dashboard/PerformanceStats' import GoalStats from '@/components/dashboard/GoalStats' import ScrollDepth from '@/components/dashboard/ScrollDepth' import Campaigns from '@/components/dashboard/Campaigns' import SiteNav from '@/components/dashboard/SiteNav' import FilterBar from '@/components/dashboard/FilterBar' import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown' import EventProperties from '@/components/dashboard/EventProperties' import { type DimensionFilter, serializeFilters, parseFiltersFromURL } from '@/lib/filters' import { useDashboardOverview, useDashboardPages, useDashboardLocations, useDashboardDevices, useDashboardReferrers, useDashboardPerformance, useDashboardGoals, useRealtime, useStats, useDailyStats, useCampaigns, } from '@/lib/swr/dashboard' function loadSavedSettings(): { type?: string dateRange?: { start: string; end: string } todayInterval?: 'minute' | 'hour' multiDayInterval?: 'hour' | 'day' } | null { if (typeof window === 'undefined') return null try { const saved = localStorage.getItem('pulse_dashboard_settings') return saved ? JSON.parse(saved) : null } catch { return null } } function getInitialDateRange(): { start: string; end: string } { const settings = loadSavedSettings() if (settings?.type === 'today') { const today = formatDate(new Date()) return { start: today, end: today } } if (settings?.type === '7') return getDateRange(7) if (settings?.type === 'custom' && settings.dateRange) return settings.dateRange return getDateRange(30) } export default function SiteDashboardPage() { const params = useParams() const router = useRouter() const siteId = params.id as string // UI state - initialized from localStorage synchronously to avoid double-fetch const [dateRange, setDateRange] = useState(getInitialDateRange) const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>( () => loadSavedSettings()?.todayInterval || 'hour' ) const [multiDayInterval, setMultiDayInterval] = useState<'hour' | 'day'>( () => loadSavedSettings()?.multiDayInterval || 'day' ) const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) const [isExportModalOpen, setIsExportModalOpen] = useState(false) const [lastUpdatedAt, setLastUpdatedAt] = useState(null) const [, setTick] = useState(0) // Dimension filters state const searchParams = useSearchParams() const [filters, setFilters] = useState(() => { const raw = searchParams.get('filters') return raw ? parseFiltersFromURL(raw) : [] }) const filtersParam = useMemo(() => serializeFilters(filters), [filters]) // Selected event for property breakdown const [selectedEvent, setSelectedEvent] = useState(null) const handleAddFilter = useCallback((filter: DimensionFilter) => { setFilters(prev => { const isDuplicate = prev.some( f => f.dimension === filter.dimension && f.operator === filter.operator && f.values.join(';') === filter.values.join(';') ) if (isDuplicate) return prev return [...prev, filter] }) }, []) const handleRemoveFilter = useCallback((index: number) => { setFilters(prev => prev.filter((_, i) => i !== index)) }, []) const handleClearFilters = useCallback(() => { setFilters([]) }, []) // Fetch full suggestion list (up to 100) when a dimension is selected in the filter dropdown const handleFetchSuggestions = useCallback(async (dimension: string): Promise => { const start = dateRange.start const end = dateRange.end const f = filtersParam || undefined const limit = 100 try { const regionNames = (() => { try { return new Intl.DisplayNames(['en'], { type: 'region' }) } catch { return null } })() switch (dimension) { case 'page': { const data = await getTopPages(siteId, start, end, limit, f) return data.map(p => ({ value: p.path, label: p.path, count: p.pageviews })) } case 'referrer': { const data = await getTopReferrers(siteId, start, end, limit, f) return data.filter(r => r.referrer && r.referrer !== '').map(r => ({ value: r.referrer, label: r.referrer, count: r.pageviews })) } case 'country': { const data = await getCountries(siteId, start, end, limit, f) return data.filter(c => c.country && c.country !== 'Unknown').map(c => ({ value: c.country, label: regionNames?.of(c.country) ?? c.country, count: c.pageviews })) } case 'city': { const data = await getCities(siteId, start, end, limit, f) return data.filter(c => c.city && c.city !== 'Unknown').map(c => ({ value: c.city, label: c.city, count: c.pageviews })) } case 'region': { const data = await getRegions(siteId, start, end, limit, f) return data.filter(r => r.region && r.region !== 'Unknown').map(r => ({ value: r.region, label: r.region, count: r.pageviews })) } case 'browser': { const data = await getBrowsers(siteId, start, end, limit, f) return data.filter(b => b.browser && b.browser !== 'Unknown').map(b => ({ value: b.browser, label: b.browser, count: b.pageviews })) } case 'os': { const data = await getOS(siteId, start, end, limit, f) return data.filter(o => o.os && o.os !== 'Unknown').map(o => ({ value: o.os, label: o.os, count: o.pageviews })) } case 'device': { const data = await getDevices(siteId, start, end, limit, f) return data.filter(d => d.device && d.device !== 'Unknown').map(d => ({ value: d.device, label: d.device, count: d.pageviews })) } case 'utm_source': case 'utm_medium': case 'utm_campaign': { const data = await getCampaigns(siteId, start, end, limit, f) const map = new Map() const field = dimension === 'utm_source' ? 'source' : dimension === 'utm_medium' ? 'medium' : 'campaign' data.forEach(c => { const val = c[field] if (val) map.set(val, (map.get(val) ?? 0) + c.pageviews) }) return [...map.entries()].map(([v, count]) => ({ value: v, label: v, count })) } default: return [] } } catch { return [] } }, [siteId, dateRange.start, dateRange.end, filtersParam]) // Sync filters to URL useEffect(() => { const url = new URL(window.location.href) if (filtersParam) { url.searchParams.set('filters', filtersParam) } else { url.searchParams.delete('filters') } window.history.replaceState({}, '', url.toString()) }, [filtersParam]) const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval // Previous period date range for comparison const prevRange = useMemo(() => { const startDate = new Date(dateRange.start) const endDate = new Date(dateRange.end) const duration = endDate.getTime() - startDate.getTime() if (duration === 0) { const prevEnd = new Date(startDate.getTime() - 24 * 60 * 60 * 1000) return { start: prevEnd.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] } } const prevEnd = new Date(startDate.getTime() - 24 * 60 * 60 * 1000) const prevStart = new Date(prevEnd.getTime() - duration) return { start: prevStart.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] } }, [dateRange]) // SWR hooks - replace manual useState + useEffect + setInterval polling // Each hook handles its own refresh interval, deduplication, and error retry // Filters are included in cache keys so changing filters auto-refetches const { data: overview, isLoading: overviewLoading, error: overviewError } = useDashboardOverview(siteId, dateRange.start, dateRange.end, interval, filtersParam || undefined) const { data: pages } = useDashboardPages(siteId, dateRange.start, dateRange.end, filtersParam || undefined) const { data: locations } = useDashboardLocations(siteId, dateRange.start, dateRange.end, filtersParam || undefined) const { data: devicesData } = useDashboardDevices(siteId, dateRange.start, dateRange.end, filtersParam || undefined) const { data: referrers } = useDashboardReferrers(siteId, dateRange.start, dateRange.end, filtersParam || undefined) const { data: performanceData } = useDashboardPerformance(siteId, dateRange.start, dateRange.end, filtersParam || undefined) const { data: goalsData } = useDashboardGoals(siteId, dateRange.start, dateRange.end, filtersParam || undefined) const { data: realtimeData } = useRealtime(siteId) const { data: prevStats } = useStats(siteId, prevRange.start, prevRange.end) const { data: prevDailyStats } = useDailyStats(siteId, prevRange.start, prevRange.end, interval) const { data: campaigns } = useCampaigns(siteId, dateRange.start, dateRange.end) // Derive typed values from SWR data const site = overview?.site ?? null const stats: Stats = overview?.stats ?? { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 } const realtime = realtimeData?.visitors ?? overview?.realtime_visitors ?? 0 const dailyStats: DailyStat[] = overview?.daily_stats ?? [] // Build filter suggestions from current dashboard data const filterSuggestions = useMemo(() => { const s: FilterSuggestions = {} // Pages const topPages = pages?.top_pages ?? [] if (topPages.length > 0) { s.page = topPages.map(p => ({ value: p.path, label: p.path, count: p.pageviews })) } // Referrers const refs = referrers?.top_referrers ?? [] if (refs.length > 0) { s.referrer = refs.filter(r => r.referrer && r.referrer !== '').map(r => ({ value: r.referrer, label: r.referrer, count: r.pageviews, })) } // Countries const ctrs = locations?.countries ?? [] if (ctrs.length > 0) { const regionNames = (() => { try { return new Intl.DisplayNames(['en'], { type: 'region' }) } catch { return null } })() s.country = ctrs.filter(c => c.country && c.country !== 'Unknown').map(c => ({ value: c.country, label: regionNames?.of(c.country) ?? c.country, count: c.pageviews, })) } // Regions const regs = locations?.regions ?? [] if (regs.length > 0) { s.region = regs.filter(r => r.region && r.region !== 'Unknown').map(r => ({ value: r.region, label: r.region, count: r.pageviews, })) } // Cities const cts = locations?.cities ?? [] if (cts.length > 0) { s.city = cts.filter(c => c.city && c.city !== 'Unknown').map(c => ({ value: c.city, label: c.city, count: c.pageviews, })) } // Browsers const brs = devicesData?.browsers ?? [] if (brs.length > 0) { s.browser = brs.filter(b => b.browser && b.browser !== 'Unknown').map(b => ({ value: b.browser, label: b.browser, count: b.pageviews, })) } // OS const oses = devicesData?.os ?? [] if (oses.length > 0) { s.os = oses.filter(o => o.os && o.os !== 'Unknown').map(o => ({ value: o.os, label: o.os, count: o.pageviews, })) } // Devices const devs = devicesData?.devices ?? [] if (devs.length > 0) { s.device = devs.filter(d => d.device && d.device !== 'Unknown').map(d => ({ value: d.device, label: d.device, count: d.pageviews, })) } // UTM from campaigns const camps = campaigns ?? [] if (camps.length > 0) { const sources = new Map() const mediums = new Map() const campNames = new Map() camps.forEach(c => { if (c.source) sources.set(c.source, (sources.get(c.source) ?? 0) + c.pageviews) if (c.medium) mediums.set(c.medium, (mediums.get(c.medium) ?? 0) + c.pageviews) if (c.campaign) campNames.set(c.campaign, (campNames.get(c.campaign) ?? 0) + c.pageviews) }) if (sources.size > 0) s.utm_source = [...sources.entries()].map(([v, c]) => ({ value: v, label: v, count: c })) if (mediums.size > 0) s.utm_medium = [...mediums.entries()].map(([v, c]) => ({ value: v, label: v, count: c })) if (campNames.size > 0) s.utm_campaign = [...campNames.entries()].map(([v, c]) => ({ value: v, label: v, count: c })) } return s }, [pages, referrers, locations, devicesData, campaigns]) // Show error toast on fetch failure useEffect(() => { if (overviewError) { toast.error('Failed to load dashboard analytics') } }, [overviewError]) // Track when data was last updated (for "Live · Xs ago" display) useEffect(() => { if (overview) setLastUpdatedAt(Date.now()) }, [overview]) // Tick every 1s so "Live · Xs ago" counts in real time useEffect(() => { const timer = setInterval(() => setTick((t) => t + 1), 1000) return () => clearInterval(timer) }, []) // Save settings to localStorage const saveSettings = (type: string, newDateRange?: { start: string; end: string }) => { try { const settings = { type, dateRange: newDateRange || dateRange, todayInterval, multiDayInterval, lastUpdated: Date.now() } localStorage.setItem('pulse_dashboard_settings', JSON.stringify(settings)) } catch (e) { logger.error('Failed to save dashboard settings', e) } } // Save intervals when they change useEffect(() => { let type = 'custom' const today = formatDate(new Date()) if (dateRange.start === today && dateRange.end === today) type = 'today' else if (dateRange.start === getDateRange(7).start) type = '7' else if (dateRange.start === getDateRange(30).start) type = '30' const settings = { type, dateRange, todayInterval, multiDayInterval, lastUpdated: Date.now() } localStorage.setItem('pulse_dashboard_settings', JSON.stringify(settings)) }, [todayInterval, multiDayInterval]) // eslint-disable-line react-hooks/exhaustive-deps -- dateRange saved via saveSettings useEffect(() => { if (site?.domain) document.title = `${site.domain} | Pulse` }, [site?.domain]) const showSkeleton = useMinimumLoading(overviewLoading) if (showSkeleton) { return } if (!site) { return (

Site not found

) } return (

{site.name}

{site.domain}

{/* Realtime Indicator */}