From 2aedc656d76e0a63c51623fd3e0ef5944a20290e Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sun, 22 Feb 2026 00:20:54 +0100 Subject: [PATCH] feat: implement site statistics fetching and display in SiteList component --- app/page.tsx | 44 ++++++- components/dashboard/Chart.tsx | 46 +------ components/dashboard/Sparkline.tsx | 50 +++++++ components/sites/SiteList.tsx | 202 +++++++++++++++++------------ 4 files changed, 215 insertions(+), 127 deletions(-) create mode 100644 components/dashboard/Sparkline.tsx diff --git a/app/page.tsx b/app/page.tsx index 02d85b4..3eb2f7f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,6 +6,8 @@ import { motion } from 'framer-motion' import { useAuth } from '@/lib/auth/context' import { initiateOAuthFlow, initiateSignupFlow } from '@/lib/api/oauth' import { listSites, deleteSite, type Site } from '@/lib/api/sites' +import { getStats, getDailyStats } from '@/lib/api/stats' +import type { Stats, DailyStat } from '@/lib/api/stats' import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing' import { LoadingOverlay } from '@ciphera-net/ui' import SiteList from '@/components/sites/SiteList' @@ -97,10 +99,13 @@ function ComparisonSection() { } +type SiteStatsMap = Record + export default function HomePage() { const { user, loading: authLoading } = useAuth() const [sites, setSites] = useState([]) const [sitesLoading, setSitesLoading] = useState(true) + const [siteStats, setSiteStats] = useState({}) const [subscription, setSubscription] = useState(null) const [subscriptionLoading, setSubscriptionLoading] = useState(false) const [showFinishSetupBanner, setShowFinishSetupBanner] = useState(true) @@ -112,6 +117,37 @@ export default function HomePage() { } }, [user]) + useEffect(() => { + if (sites.length === 0) { + setSiteStats({}) + return + } + let cancelled = false + const today = new Date().toISOString().split('T')[0] + const start7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + const load = async () => { + const results = await Promise.allSettled( + sites.map(async (site) => { + const [statsRes, dailyRes] = await Promise.all([ + getStats(site.id, today, today), + getDailyStats(site.id, start7d, today, 'day'), + ]) + return { siteId: site.id, stats: statsRes, dailyStats: dailyRes ?? [] } + }) + ) + if (cancelled) return + const map: SiteStatsMap = {} + for (const r of results) { + if (r.status === 'fulfilled') { + map[r.value.siteId] = { stats: r.value.stats, dailyStats: r.value.dailyStats } + } + } + setSiteStats(map) + } + load() + return () => { cancelled = true } + }, [sites]) + useEffect(() => { if (typeof window === 'undefined') return if (localStorage.getItem('pulse_welcome_completed') === 'true') setShowFinishSetupBanner(false) @@ -370,7 +406,11 @@ export default function HomePage() {

Total Visitors (24h)

-

--

+

+ {sites.length === 0 || Object.keys(siteStats).length < sites.length + ? '--' + : Object.values(siteStats).reduce((sum, { stats }) => sum + (stats?.visitors ?? 0), 0).toLocaleString()} +

Plan & usage

@@ -456,7 +496,7 @@ export default function HomePage() { )} {(sitesLoading || sites.length > 0) && ( - + )}
) diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index 355090f..792f2de 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -14,6 +14,7 @@ import { } from 'recharts' import type { TooltipProps } from 'recharts' import { formatNumber, formatDuration, formatUpdatedAgo } from '@ciphera-net/ui' +import Sparkline from './Sparkline' import { ArrowUpRightIcon, ArrowDownRightIcon, BarChartIcon, Select, Button, DownloadIcon } from '@ciphera-net/ui' import { Checkbox } from '@ciphera-net/ui' @@ -208,51 +209,6 @@ function getTrendContext(dateRange: { start: string; end: string }): string { return `vs previous ${days} days` } -// * Mini sparkline SVG for KPI cards -function Sparkline({ - data, - dataKey, - color, - width = 56, - height = 20, -}: { - data: Array> - dataKey: string - color: string - width?: number - height?: number -}) { - if (!data.length) return null - const values = data.map((d) => Number(d[dataKey] ?? 0)) - const max = Math.max(...values, 1) - const min = Math.min(...values, 0) - const range = max - min || 1 - const padding = 2 - const w = width - padding * 2 - const h = height - padding * 2 - - const points = values.map((v, i) => { - const x = padding + (i / Math.max(values.length - 1, 1)) * w - const y = padding + h - ((v - min) / range) * h - return `${x},${y}` - }) - - const pathD = points.length > 1 ? `M ${points.join(' L ')}` : `M ${points[0]} L ${points[0]}` - - return ( - - - - ) -} - export default function Chart({ data, prevData, diff --git a/components/dashboard/Sparkline.tsx b/components/dashboard/Sparkline.tsx new file mode 100644 index 0000000..f87a203 --- /dev/null +++ b/components/dashboard/Sparkline.tsx @@ -0,0 +1,50 @@ +'use client' + +/** + * Mini sparkline SVG for KPI cards. + * Renders a line chart from an array of data points. + */ +export default function Sparkline({ + data, + dataKey, + color, + width = 56, + height = 20, +}: { + /** Array of objects with numeric values (e.g. DailyStat with visitors, pageviews) */ + data: ReadonlyArray + dataKey: string + color: string + width?: number + height?: number +}) { + if (!data.length) return null + const values = data.map((d) => Number((d as Record)[dataKey] ?? 0)) + const max = Math.max(...values, 1) + const min = Math.min(...values, 0) + const range = max - min || 1 + const padding = 2 + const w = width - padding * 2 + const h = height - padding * 2 + + const points = values.map((v, i) => { + const x = padding + (i / Math.max(values.length - 1, 1)) * w + const y = padding + h - ((v - min) / range) * h + return `${x},${y}` + }) + + const pathD = points.length > 1 ? `M ${points.join(' L ')}` : `M ${points[0]} L ${points[0]}` + + return ( + + + + ) +} diff --git a/components/sites/SiteList.tsx b/components/sites/SiteList.tsx index b88004f..13eb50d 100644 --- a/components/sites/SiteList.tsx +++ b/components/sites/SiteList.tsx @@ -2,17 +2,124 @@ import Link from 'next/link' import { Site } from '@/lib/api/sites' +import type { Stats, DailyStat } from '@/lib/api/stats' +import { formatNumber } from '@ciphera-net/ui' import { BarChartIcon, SettingsIcon, BookOpenIcon, ExternalLinkIcon, Button } from '@ciphera-net/ui' import { useAuth } from '@/lib/auth/context' +import Sparkline from '@/components/dashboard/Sparkline' + +export type SiteStatsMap = Record interface SiteListProps { sites: Site[] + siteStats: SiteStatsMap loading: boolean onDelete: (id: string) => void } -export default function SiteList({ sites, loading, onDelete }: SiteListProps) { +interface SiteCardProps { + site: Site + stats: Stats | null + dailyStats: DailyStat[] + statsLoading: boolean + onDelete: (id: string) => void + canDelete: boolean +} + +function SiteCard({ site, stats, dailyStats, statsLoading, onDelete, canDelete }: SiteCardProps) { + const visitors24h = stats?.visitors ?? 0 + const pageviews = stats?.pageviews ?? 0 + const hasChartData = dailyStats.length > 0 + const sparklineColor = 'var(--color-brand-orange)' + + return ( +
+ {/* Header: Icon + Name + Live Status */} +
+
+
+ {site.name} +
+
+

{site.name}

+
+ {site.domain} + e.stopPropagation()} + > + + +
+
+
+ +
+ + + + + Active +
+
+ + {/* Mini Stats Grid + KPI Chart */} +
+
+
+

Visitors (24h)

+

+ {statsLoading ? '--' : formatNumber(visitors24h)} +

+
+
+

Pageviews

+

+ {statsLoading ? '--' : formatNumber(pageviews)} +

+
+
+ {hasChartData && ( +
+ + 7d visitors +
+ )} +
+ + {/* Actions */} +
+ + + + {canDelete && ( + + )} +
+
+ ) +} + +export default function SiteList({ sites, siteStats, loading, onDelete }: SiteListProps) { const { user } = useAuth() + const canDelete = user?.role === 'owner' || user?.role === 'admin' if (loading) { return ( @@ -40,85 +147,20 @@ export default function SiteList({ sites, loading, onDelete }: SiteListProps) { return (
- {sites.map((site) => ( -
- {/* Header: Icon + Name + Live Status */} -
-
- {/* Auto-fetch favicon */} -
- {site.name} -
-
-

{site.name}

-
- {site.domain} - e.stopPropagation()} - > - - -
-
-
- - {/* "Live" Indicator */} -
- - - - - Active -
-
- - {/* Mini Stats Grid */} -
-
-

Visitors (24h)

-

--

-
-
-

Pageviews

-

--

-
-
- - {/* Actions */} -
- - - - {(user?.role === 'owner' || user?.role === 'admin') && ( - - )} -
-
- ))} + {sites.map((site) => { + const data = siteStats[site.id] + return ( + + ) + })} {/* Resources Card */}