feat: implement last updated timestamp display in dashboard components for improved data freshness indication
This commit is contained in:
@@ -3,7 +3,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams, useSearchParams, useRouter } from 'next/navigation'
|
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 { 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 { toast } from '@ciphera-net/ui'
|
||||||
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
|
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
|
||||||
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
||||||
@@ -293,22 +292,15 @@ export default function PublicDashboardPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Realtime Indicator & Polling - Desktop */}
|
{/* Realtime Indicator - Desktop */}
|
||||||
<div className="hidden md:flex items-center gap-3 self-end mb-1">
|
<div className="hidden md:flex items-center gap-2 px-3 py-1 bg-green-500/10 rounded-full border border-green-500/20 self-end mb-1">
|
||||||
<div className="flex items-center gap-2 px-3 py-1 bg-green-500/10 rounded-full border border-green-500/20">
|
<span className="relative flex h-2 w-2">
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span>
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
</span>
|
||||||
</span>
|
<span className="text-sm font-medium text-green-700 dark:text-green-400">
|
||||||
<span className="text-sm font-medium text-green-700 dark:text-green-400">
|
{realtime_visitors} current visitors
|
||||||
{realtime_visitors} current visitors
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{lastUpdatedAt !== null && (
|
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400" title="Data refreshes every 30 seconds">
|
|
||||||
Updated {formatUpdatedAgo(lastUpdatedAt)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,6 +380,7 @@ export default function PublicDashboardPage() {
|
|||||||
setTodayInterval={setTodayInterval}
|
setTodayInterval={setTodayInterval}
|
||||||
multiDayInterval={multiDayInterval}
|
multiDayInterval={multiDayInterval}
|
||||||
setMultiDayInterval={setMultiDayInterval}
|
setMultiDayInterval={setMultiDayInterval}
|
||||||
|
lastUpdatedAt={lastUpdatedAt}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useParams, useRouter } from 'next/navigation'
|
|||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { getSite, type Site } from '@/lib/api/sites'
|
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 { 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, formatUpdatedAgo, getDateRange } from '@/lib/utils/format'
|
import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format'
|
||||||
import { toast } from '@ciphera-net/ui'
|
import { toast } from '@ciphera-net/ui'
|
||||||
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
|
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
|
||||||
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
||||||
@@ -259,27 +259,19 @@ export default function SiteDashboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
{/* Realtime Indicator */}
|
||||||
{/* Realtime Indicator */}
|
<button
|
||||||
<button
|
onClick={() => router.push(`/sites/${siteId}/realtime`)}
|
||||||
onClick={() => router.push(`/sites/${siteId}/realtime`)}
|
className="flex items-center gap-2 px-3 py-1 bg-green-500/10 rounded-full border border-green-500/20 hover:bg-green-500/20 transition-colors cursor-pointer"
|
||||||
className="flex items-center gap-2 px-3 py-1 bg-green-500/10 rounded-full border border-green-500/20 hover:bg-green-500/20 transition-colors cursor-pointer"
|
>
|
||||||
>
|
<span className="relative flex h-2 w-2">
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span>
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span>
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
</span>
|
||||||
</span>
|
<span className="text-sm font-medium text-green-700 dark:text-green-400">
|
||||||
<span className="text-sm font-medium text-green-700 dark:text-green-400">
|
{realtime} current visitors
|
||||||
{realtime} current visitors
|
</span>
|
||||||
</span>
|
</button>
|
||||||
</button>
|
|
||||||
{/* Polling indicator */}
|
|
||||||
{lastUpdatedAt !== null && (
|
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400" title="Data refreshes every 30 seconds">
|
|
||||||
Updated {formatUpdatedAgo(lastUpdatedAt)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -379,6 +371,7 @@ export default function SiteDashboardPage() {
|
|||||||
setTodayInterval={setTodayInterval}
|
setTodayInterval={setTodayInterval}
|
||||||
multiDayInterval={multiDayInterval}
|
multiDayInterval={multiDayInterval}
|
||||||
setMultiDayInterval={setMultiDayInterval}
|
setMultiDayInterval={setMultiDayInterval}
|
||||||
|
lastUpdatedAt={lastUpdatedAt}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
ReferenceLine,
|
ReferenceLine,
|
||||||
} from 'recharts'
|
} from 'recharts'
|
||||||
import type { TooltipProps } from 'recharts'
|
import type { TooltipProps } from 'recharts'
|
||||||
import { formatNumber, formatDuration } from '@/lib/utils/format'
|
import { formatNumber, formatDuration, formatUpdatedAgo } from '@/lib/utils/format'
|
||||||
import { ArrowUpRightIcon, ArrowDownRightIcon, BarChartIcon, Select, Button, DownloadIcon } from '@ciphera-net/ui'
|
import { ArrowUpRightIcon, ArrowDownRightIcon, BarChartIcon, Select, Button, DownloadIcon } from '@ciphera-net/ui'
|
||||||
import { Checkbox } from '@ciphera-net/ui'
|
import { Checkbox } from '@ciphera-net/ui'
|
||||||
|
|
||||||
@@ -69,6 +69,8 @@ interface ChartProps {
|
|||||||
setMultiDayInterval: (interval: 'hour' | 'day') => void
|
setMultiDayInterval: (interval: 'hour' | 'day') => void
|
||||||
/** Optional: callback when user requests chart export (parent can open ExportModal or handle export) */
|
/** Optional: callback when user requests chart export (parent can open ExportModal or handle export) */
|
||||||
onExportChart?: () => void
|
onExportChart?: () => void
|
||||||
|
/** Optional: timestamp of last data fetch for "Live · Xs ago" indicator */
|
||||||
|
lastUpdatedAt?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetricType = 'pageviews' | 'visitors' | 'bounce_rate' | 'avg_duration'
|
type MetricType = 'pageviews' | 'visitors' | 'bounce_rate' | 'avg_duration'
|
||||||
@@ -263,6 +265,7 @@ export default function Chart({
|
|||||||
multiDayInterval,
|
multiDayInterval,
|
||||||
setMultiDayInterval,
|
setMultiDayInterval,
|
||||||
onExportChart,
|
onExportChart,
|
||||||
|
lastUpdatedAt,
|
||||||
}: ChartProps) {
|
}: ChartProps) {
|
||||||
const [metric, setMetric] = useState<MetricType>('visitors')
|
const [metric, setMetric] = useState<MetricType>('visitors')
|
||||||
const [showComparison, setShowComparison] = useState(false)
|
const [showComparison, setShowComparison] = useState(false)
|
||||||
@@ -406,10 +409,19 @@ export default function Chart({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={chartContainerRef}
|
ref={chartContainerRef}
|
||||||
className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden shadow-sm"
|
className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden shadow-sm relative"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={`Analytics chart showing ${metricLabel} over time`}
|
aria-label={`Analytics chart showing ${metricLabel} over time`}
|
||||||
>
|
>
|
||||||
|
{/* * Subtle live/updated indicator in bottom-right corner */}
|
||||||
|
{lastUpdatedAt != null && (
|
||||||
|
<div
|
||||||
|
className="absolute bottom-3 right-6 text-xs text-neutral-500 dark:text-neutral-400 pointer-events-none"
|
||||||
|
title="Data refreshes every 30 seconds"
|
||||||
|
>
|
||||||
|
Live · {formatUpdatedAgo(lastUpdatedAt)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Stats Header (Interactive Tabs) */}
|
{/* Stats Header (Interactive Tabs) */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 divide-x divide-neutral-200 dark:divide-neutral-800 border-b border-neutral-200 dark:border-neutral-800">
|
<div className="grid grid-cols-2 md:grid-cols-4 divide-x divide-neutral-200 dark:divide-neutral-800 border-b border-neutral-200 dark:border-neutral-800">
|
||||||
{metrics.map((item) => (
|
{metrics.map((item) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user