'use client' import { useCallback, useEffect, useState } from 'react' import { useParams, useSearchParams, useRouter } from 'next/navigation' import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@ciphera-net/ui' import { LoadingOverlay, Button } from '@ciphera-net/ui' import Chart from '@/components/dashboard/Chart' import TopPages from '@/components/dashboard/ContentStats' import TopReferrers from '@/components/dashboard/TopReferrers' import Locations from '@/components/dashboard/Locations' import TechSpecs from '@/components/dashboard/TechSpecs' import PerformanceStats from '@/components/dashboard/PerformanceStats' import { Select, DatePicker as DatePickerModal, Captcha, DownloadIcon, ZapIcon } from '@ciphera-net/ui' import { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons' import ExportModal from '@/components/dashboard/ExportModal' // Helper to get date ranges const getDateRange = (days: number) => { const end = new Date() const start = new Date() start.setDate(end.getDate() - (days - 1)) // -1 because today counts as 1 day return { start: start.toISOString().split('T')[0], end: end.toISOString().split('T')[0] } } export default function PublicDashboardPage() { const params = useParams() const searchParams = useSearchParams() const router = useRouter() const siteId = params.id as string const passwordParam = searchParams.get('password') || undefined const [loading, setLoading] = useState(true) const [data, setData] = useState(null) const [password, setPassword] = useState(passwordParam || '') const [isPasswordProtected, setIsPasswordProtected] = useState(false) // Captcha State const [captchaId, setCaptchaId] = useState('') const [captchaSolution, setCaptchaSolution] = useState('') const [captchaToken, setCaptchaToken] = useState('') // Date range state const [dateRange, setDateRange] = useState(getDateRange(30)) const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) const [isExportModalOpen, setIsExportModalOpen] = useState(false) const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>('hour') const [multiDayInterval, setMultiDayInterval] = useState<'hour' | 'day'>('day') // Previous period data const [prevStats, setPrevStats] = useState(undefined) const [prevDailyStats, setPrevDailyStats] = useState(undefined) const [lastUpdatedAt, setLastUpdatedAt] = useState(null) const [, setTick] = useState(0) const getPreviousDateRange = (start: string, end: string) => { const startDate = new Date(start) const endDate = new Date(end) const duration = endDate.getTime() - startDate.getTime() // * If duration is 0 (Today), set previous range to yesterday if (duration === 0) { const prevEnd = new Date(startDate.getTime() - 24 * 60 * 60 * 1000) const prevStart = prevEnd return { start: prevStart.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] } } // * Tick every 1s so "Live ยท Xs ago" counts in real time useEffect(() => { const interval = setInterval(() => setTick((t) => t + 1), 1000) return () => clearInterval(interval) }, []) const loadRealtime = useCallback(async () => { try { const auth = { password, captcha: { captcha_id: captchaId, captcha_solution: captchaSolution, captcha_token: captchaToken } } const realtimeData = await getPublicRealtime(siteId, auth) if (data) { setData({ ...data, realtime_visitors: realtimeData.visitors }) } } catch (error) { // Silently fail for realtime updates } }, [siteId, password, captchaId, captchaSolution, captchaToken, data]) const loadDashboard = useCallback(async (silent = false) => { try { if (!silent) setLoading(true) const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval const auth = { password, captcha: { captcha_id: captchaId, captcha_solution: captchaSolution, captcha_token: captchaToken } } const [dashboardData, prevStatsData, prevDailyStatsData] = await Promise.all([ getPublicDashboard( siteId, dateRange.start, dateRange.end, 10, interval, password, auth.captcha ), (async () => { const prevRange = getPreviousDateRange(dateRange.start, dateRange.end) return getPublicStats(siteId, prevRange.start, prevRange.end, auth) })(), (async () => { const prevRange = getPreviousDateRange(dateRange.start, dateRange.end) return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval, auth) })() ]) setData(dashboardData) setPrevStats(prevStatsData) setPrevDailyStats(prevDailyStatsData) setLastUpdatedAt(Date.now()) setIsPasswordProtected(false) // Reset captcha setCaptchaId('') setCaptchaSolution('') setCaptchaToken('') } catch (error: any) { if ((error.status === 401 || error.response?.status === 401) && (error.data?.is_protected || error.response?.data?.is_protected)) { setIsPasswordProtected(true) if (password) { toast.error('Invalid password or captcha') // Reset captcha on failure setCaptchaId('') setCaptchaSolution('') setCaptchaToken('') } } else if (error.status === 404 || error.response?.status === 404) { toast.error('Site not found') } else if (!silent) { toast.error(getAuthErrorMessage(error) || 'Failed to load public dashboard') } } finally { if (!silent) setLoading(false) } }, [siteId, dateRange, todayInterval, multiDayInterval, password, captchaId, captchaSolution, captchaToken]) // * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds useEffect(() => { if (data && !isPasswordProtected) { const interval = setInterval(() => { loadDashboard(true) loadRealtime() }, 30000) return () => clearInterval(interval) } }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime]) useEffect(() => { loadDashboard() }, [siteId, dateRange, todayInterval, multiDayInterval, loadDashboard]) const handlePasswordSubmit = (e: React.FormEvent) => { e.preventDefault() loadDashboard() } const showSkeleton = useMinimumLoading(loading && !data && !isPasswordProtected) if (showSkeleton) { return } if (isPasswordProtected && !data) { return (

Protected Dashboard

This dashboard is password protected. Please enter the password to view stats.

setPassword(e.target.value)} placeholder="Enter password" className="w-full px-4 py-2 border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white focus:ring-2 focus:ring-brand-orange focus:border-transparent" autoFocus />
{ setCaptchaId(id) setCaptchaSolution(solution) setCaptchaToken(token || '') }} apiUrl={process.env.NEXT_PUBLIC_CAPTCHA_API_URL} />
) } if (!data) return null const { site, stats, daily_stats, top_pages, entry_pages, exit_pages, top_referrers, countries, cities, regions, browsers, os, devices, screen_resolutions, performance, performance_by_page, realtime_visitors } = data // Provide defaults for potentially undefined data const safeDailyStats = daily_stats || [] const safeStats = stats || { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 } const safeTopPages = top_pages || [] const safeEntryPages = entry_pages || [] const safeExitPages = exit_pages || [] const safeTopReferrers = top_referrers || [] const safeCountries = countries || [] const safeCities = cities || [] const safeRegions = regions || [] const safeBrowsers = browsers || [] const safeOS = os || [] const safeDevices = devices || [] const safeScreenResolutions = screen_resolutions || [] return (
{/* Header */}
Public Dashboard

{site.name} { (e.target as HTMLImageElement).src = '/globe.svg' }} /> {site.domain}

{/* Realtime Indicator - Desktop */}
{realtime_visitors} current visitors