feat(pagespeed): add PageSpeed page with gauges, CWV cards, chart, and diagnostics

- ScoreGauge SVG component with color-coded circular arcs
- Full page: disabled state, score overview, CWV metrics, trend chart
- Diagnostics accordion with opportunities/diagnostics/passed groups
- Mobile/desktop strategy toggle, manual check trigger
- Loading skeleton, frequency selector
This commit is contained in:
Usman Baig
2026-03-22 18:13:08 +01:00
parent 780dd464a1
commit 52906344cf
3 changed files with 561 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ import { getUserOrganizations, switchContext, type OrganizationMember } from '@/
import { setSessionAction } from '@/app/actions/auth'
import { logger } from '@/lib/utils/logger'
import { FAVICON_SERVICE_URL } from '@/lib/utils/icons'
import { Gauge as GaugeIcon } from '@phosphor-icons/react'
import {
LayoutDashboardIcon,
PathIcon,
@@ -18,7 +19,6 @@ import {
SearchIcon,
CloudUploadIcon,
HeartbeatIcon,
GaugeIcon,
SettingsIcon,
CollapseLeftIcon,
CollapseRightIcon,

View File

@@ -0,0 +1,71 @@
'use client'
interface ScoreGaugeProps {
score: number | null
label: string
}
const RADIUS = 44
const CIRCUMFERENCE = 2 * Math.PI * RADIUS
function getColor(score: number): string {
if (score >= 90) return '#0cce6b'
if (score >= 50) return '#ffa400'
return '#ff4e42'
}
export default function ScoreGauge({ score, label }: ScoreGaugeProps) {
const hasScore = score !== null && score !== undefined
const displayScore = hasScore ? Math.round(score) : null
const offset = hasScore ? CIRCUMFERENCE * (1 - score / 100) : CIRCUMFERENCE
const color = hasScore ? getColor(score) : '#6b7280'
return (
<div className="flex flex-col items-center gap-2">
<div className="relative w-[120px] h-[120px]">
<svg
className="w-full h-full -rotate-90"
viewBox="0 0 100 100"
>
{/* Track */}
<circle
cx="50"
cy="50"
r={RADIUS}
fill="none"
stroke="currentColor"
className="text-neutral-200 dark:text-neutral-700"
strokeWidth="8"
/>
{/* Filled arc */}
<circle
cx="50"
cy="50"
r={RADIUS}
fill="none"
stroke={color}
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={offset}
style={{ transition: 'stroke-dashoffset 0.6s ease' }}
/>
</svg>
{/* Score text */}
<div className="absolute inset-0 flex items-center justify-center">
<span
className="text-2xl font-bold"
style={{ color: hasScore ? color : undefined }}
>
{displayScore !== null ? displayScore : (
<span className="text-neutral-400 dark:text-neutral-500">--</span>
)}
</span>
</div>
</div>
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
{label}
</span>
</div>
)
}