From 4aefca7118cb2262ee2de7fb107a40431da8d668 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 11 Feb 2026 20:49:09 +0100 Subject: [PATCH] feat: add "Updated X ago" display for realtime indicators and implement auto-refresh tick functionality --- app/share/[id]/page.tsx | 37 +++++++++++++++++++------- app/sites/[id]/page.tsx | 58 +++++++++++++++++++++++++++-------------- lib/utils/format.ts | 12 +++++++++ 3 files changed, 78 insertions(+), 29 deletions(-) diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx index 56359e8..087f558 100644 --- a/app/share/[id]/page.tsx +++ b/app/share/[id]/page.tsx @@ -3,6 +3,7 @@ import { 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 { formatUpdatedAgo } from '@/lib/utils/format' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { LoadingOverlay, Button } from '@ciphera-net/ui' @@ -53,6 +54,8 @@ export default function PublicDashboardPage() { // 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) @@ -78,17 +81,23 @@ export default function PublicDashboardPage() { } } - // Auto-refresh interval (for realtime) + // * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds useEffect(() => { const interval = setInterval(() => { - // Only refresh realtime count if we have data if (data && !isPasswordProtected) { + loadDashboard(true) loadRealtime() } - }, 30000) // 30 seconds + }, 30000) return () => clearInterval(interval) - }, [data, isPasswordProtected, dateRange, password]) + }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password]) + + // * Tick every 5s to refresh "Updated X ago" display + useEffect(() => { + const interval = setInterval(() => setTick((t) => t + 1), 5000) + return () => clearInterval(interval) + }, []) useEffect(() => { loadDashboard() @@ -153,6 +162,7 @@ export default function PublicDashboardPage() { setData(dashboardData) setPrevStats(prevStatsData) setPrevDailyStats(prevDailyStatsData) + setLastUpdatedAt(Date.now()) setIsPasswordProtected(false) // Reset captcha @@ -283,15 +293,22 @@ export default function PublicDashboardPage() { - {/* Realtime Indicator - Desktop */} -
- + {/* Realtime Indicator & Polling - Desktop */} +
+
+ - - + + {realtime_visitors} current visitors - + +
+ {lastUpdatedAt !== null && ( + + Updated {formatUpdatedAgo(lastUpdatedAt)} + + )}
diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 7d5a319..676f9d4 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -6,7 +6,7 @@ import { useParams, useRouter } from 'next/navigation' import { motion } from 'framer-motion' import { getSite, type Site } from '@/lib/api/sites' import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages, getDashboard, getCampaigns, getPerformanceByPage, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats' -import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format' +import { formatNumber, formatDuration, formatUpdatedAgo, getDateRange } from '@/lib/utils/format' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { LoadingOverlay, Button } from '@ciphera-net/ui' @@ -57,6 +57,8 @@ export default function SiteDashboardPage() { const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>('hour') const [multiDayInterval, setMultiDayInterval] = useState<'hour' | 'day'>('day') const [isSettingsLoaded, setIsSettingsLoaded] = useState(false) + const [lastUpdatedAt, setLastUpdatedAt] = useState(null) + const [, setTick] = useState(0) // Load settings from localStorage useEffect(() => { @@ -130,11 +132,18 @@ export default function SiteDashboardPage() { loadData() } const interval = setInterval(() => { + loadData(true) loadRealtime() - }, 30000) // Update every 30 seconds + }, 30000) // * Chart, KPIs, and realtime count update every 30 seconds return () => clearInterval(interval) }, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded]) + // * Tick every 5s to refresh "Updated X ago" display + useEffect(() => { + const interval = setInterval(() => setTick((t) => t + 1), 5000) + return () => clearInterval(interval) + }, []) + const getPreviousDateRange = (start: string, end: string) => { const startDate = new Date(start) const endDate = new Date(end) @@ -159,9 +168,9 @@ export default function SiteDashboardPage() { } } - const loadData = async () => { + const loadData = async (silent = false) => { try { - setLoading(true) + if (!silent) setLoading(true) const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval const [data, prevStatsData, prevDailyStatsData, campaignsData] = await Promise.all([ @@ -200,10 +209,13 @@ export default function SiteDashboardPage() { setPerformanceByPage(data.performance_by_page ?? null) setGoalCounts(Array.isArray(data.goal_counts) ? data.goal_counts : []) setCampaigns(Array.isArray(campaignsData) ? campaignsData : []) + setLastUpdatedAt(Date.now()) } catch (error: unknown) { - toast.error(getAuthErrorMessage(error) || 'Failed to load data: ' + ((error as Error)?.message || 'Unknown error')) + if (!silent) { + toast.error(getAuthErrorMessage(error) || 'Failed to load data: ' + ((error as Error)?.message || 'Unknown error')) + } } finally { - setLoading(false) + if (!silent) setLoading(false) } } @@ -247,19 +259,27 @@ export default function SiteDashboardPage() {

- {/* Realtime Indicator */} - +
+ {/* Realtime Indicator */} + + {/* Polling indicator */} + {lastUpdatedAt !== null && ( + + Updated {formatUpdatedAgo(lastUpdatedAt)} + + )} +
diff --git a/lib/utils/format.ts b/lib/utils/format.ts index fb0a200..485efa6 100644 --- a/lib/utils/format.ts +++ b/lib/utils/format.ts @@ -25,6 +25,18 @@ export function getDateRange(days: number): { start: string; end: string } { } } +/** + * Format "updated X ago" for polling indicators (e.g. "Just now", "12 seconds ago") + */ +export function formatUpdatedAgo(timestamp: number): string { + const diff = Math.floor((Date.now() - timestamp) / 1000) + if (diff < 5) return 'Just now' + if (diff < 60) return `${diff} seconds ago` + if (diff < 120) return '1 minute ago' + const minutes = Math.floor(diff / 60) + return `${minutes} minutes ago` +} + /** * Format relative time (e.g., "2 hours ago") */