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:
@@ -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,
|
||||
|
||||
71
components/pagespeed/ScoreGauge.tsx
Normal file
71
components/pagespeed/ScoreGauge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user