-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCoreWebVitalsCard.tsx
More file actions
125 lines (115 loc) · 3.7 KB
/
Copy pathCoreWebVitalsCard.tsx
File metadata and controls
125 lines (115 loc) · 3.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
import type { LighthousePageScore } from './types';
interface CoreWebVitalsCardProps {
data: LighthousePageScore[];
}
interface VitalConfig {
label: string;
key: keyof LighthousePageScore;
unit: string;
thresholds: { good: number; poor: number };
parse: (value: string) => number;
}
const vitals: VitalConfig[] = [
{
label: 'LCP',
key: 'lcp',
unit: 's',
thresholds: { good: 2.5, poor: 4.0 },
parse: (v) => parseFloat(v.replace(/[^\d.]/g, '')),
},
{
label: 'FCP',
key: 'fcp',
unit: 's',
thresholds: { good: 1.8, poor: 3.0 },
parse: (v) => parseFloat(v.replace(/[^\d.]/g, '')),
},
{
label: 'CLS',
key: 'cls',
unit: '',
thresholds: { good: 0.1, poor: 0.25 },
parse: (v) => parseFloat(v) || 0,
},
{
label: 'TBT',
key: 'tbt',
unit: 'ms',
thresholds: { good: 200, poor: 600 },
parse: (v) => parseFloat(v.replace(/[^\d.]/g, '')),
},
];
function getStatus(value: number, thresholds: { good: number; poor: number }): 'good' | 'warning' | 'critical' {
if (value <= thresholds.good) return 'good';
if (value <= thresholds.poor) return 'warning';
return 'critical';
}
export function CoreWebVitalsCard({ data }: CoreWebVitalsCardProps) {
if (data.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Core Web Vitals</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">No data available</p>
</CardContent>
</Card>
);
}
// Use home page metrics as primary, or first available
const homePage = data.find(p => p.page === 'home') ?? data[0];
return (
<Card>
<CardHeader>
<CardTitle>Core Web Vitals</CardTitle>
<p className="text-sm text-muted-foreground">
Based on {homePage.page} page
</p>
</CardHeader>
<CardContent className="space-y-4">
{vitals.map((vital) => {
const rawValue = homePage[vital.key] as string;
const value = vital.parse(rawValue);
const status = getStatus(value, vital.thresholds);
// Calculate progress percentage (0-100 based on poor threshold)
const progressValue = Math.min((value / vital.thresholds.poor) * 100, 100);
const statusText = {
good: 'Good',
warning: 'Needs Improvement',
critical: 'Poor',
};
return (
<div key={vital.key} className="space-y-1">
<div className="flex justify-between text-sm">
<span className="font-medium">{vital.label}</span>
<span className={cn('font-mono tabular-nums', {
'text-emerald-500': status === 'good',
'text-amber-500': status === 'warning',
'text-red-500': status === 'critical',
})}>
{rawValue}
<span className="text-muted-foreground text-xs ml-2">
({statusText[status]})
</span>
</span>
</div>
<Progress
value={progressValue}
className="h-2"
aria-label={`${vital.label}: ${rawValue}`}
/>
<div className="flex justify-between text-xs text-muted-foreground tabular-nums">
<span>Good: ≤{vital.thresholds.good}{vital.unit}</span>
<span>Poor: >{vital.thresholds.poor}{vital.unit}</span>
</div>
</div>
);
})}
</CardContent>
</Card>
);
}