From 78d348f037b187afc1c29f3390eb02b71cd820a8 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 19 Jan 2026 18:15:45 +0100 Subject: [PATCH] feat: enhance PerformanceStats component with overall performance scoring and improved UI for metric display --- components/dashboard/PerformanceStats.tsx | 271 +++++++++++++--------- 1 file changed, 161 insertions(+), 110 deletions(-) diff --git a/components/dashboard/PerformanceStats.tsx b/components/dashboard/PerformanceStats.tsx index 28aeb33..c8e5fff 100644 --- a/components/dashboard/PerformanceStats.tsx +++ b/components/dashboard/PerformanceStats.tsx @@ -35,13 +35,33 @@ function MetricCard({ label, value, unit, score }: { label: string, value: numbe export default function PerformanceStats({ stats, performanceByPage, siteId, startDate, endDate, getPerformanceByPage }: Props) { // * Scoring Logic (based on Google Web Vitals) - const getScore = (metric: 'lcp' | 'cls' | 'inp', value: number) => { + type Score = 'good' | 'needs-improvement' | 'poor' + const getScore = (metric: 'lcp' | 'cls' | 'inp', value: number): Score => { if (metric === 'lcp') return value <= 2500 ? 'good' : value <= 4000 ? 'needs-improvement' : 'poor' if (metric === 'cls') return value <= 0.1 ? 'good' : value <= 0.25 ? 'needs-improvement' : 'poor' if (metric === 'inp') return value <= 200 ? 'good' : value <= 500 ? 'needs-improvement' : 'poor' return 'good' } + // * Overall performance: worst of LCP, CLS, INP (matches Google’s “field” rating) + const getOverallScore = (s: { lcp: number; cls: number; inp: number }): Score => { + const lcp = getScore('lcp', s.lcp) + const cls = getScore('cls', s.cls) + const inp = getScore('inp', s.inp) + if (lcp === 'poor' || cls === 'poor' || inp === 'poor') return 'poor' + if (lcp === 'needs-improvement' || cls === 'needs-improvement' || inp === 'needs-improvement') return 'needs-improvement' + return 'good' + } + + const overallScore = getOverallScore(stats) + const overallLabel = { good: 'Good', 'needs-improvement': 'Needs improvement', poor: 'Poor' }[overallScore] + const overallBadgeClass = { + good: 'text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900/30 border-green-200 dark:border-green-800', + 'needs-improvement': 'text-yellow-700 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800', + poor: 'text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 border-red-200 dark:border-red-800', + }[overallScore] + + const [mainExpanded, setMainExpanded] = useState(false) const [sortBy, setSortBy] = useState<'lcp' | 'cls' | 'inp'>('lcp') const [overrideRows, setOverrideRows] = useState(null) const [loadingTable, setLoadingTable] = useState(false) @@ -85,118 +105,149 @@ export default function PerformanceStats({ stats, performanceByPage, siteId, sta return cellScoreClass(getScore(metric, val)) } - return ( -
-

- Core Web Vitals -

-
- - - -
-
- * Averages calculated from real user sessions. Lower is better. -
+ const summaryText = `LCP ${Math.round(stats.lcp)} ms · CLS ${Number(stats.cls.toFixed(3))} · INP ${Math.round(stats.inp)} ms` - {/* * Performance by page (worst first) – collapsed by default */} -
-
- - {worstPagesOpen && canRefetch && ( - + )}
- ) : ( -
- - - - - - - - - - - - {rows.map((r) => ( - - - - - - - - ))} - -
PathSamplesLCPCLSINP
- {r.path || '/'} - {r.samples} - {formatMetric('lcp', r.lcp)} - - {formatMetric('cls', r.cls)} - - {formatMetric('inp', r.inp)} -
-
- )} - -
+ + {loadingTable ? ( +
Loading…
+ ) : rows.length === 0 ? ( +
+ No per-page metrics yet. Data appears as visitors are tracked with performance insights enabled. +
+ ) : ( +
+ + + + + + + + + + + + {rows.map((r) => ( + + + + + + + + ))} + +
PathSamplesLCPCLSINP
+ {r.path || '/'} + {r.samples} + {formatMetric('lcp', r.lcp)} + + {formatMetric('cls', r.cls)} + + {formatMetric('inp', r.inp)} +
+
+ )} +
+
+ + ) }