diff --git a/app/sites/[id]/pagespeed/page.tsx b/app/sites/[id]/pagespeed/page.tsx index 8e86056..df1f307 100644 --- a/app/sites/[id]/pagespeed/page.tsx +++ b/app/sites/[id]/pagespeed/page.tsx @@ -230,22 +230,51 @@ export default function PageSpeedPage() { // * Parse audits into groups const audits = currentCheck?.audits ?? [] - const opportunities = audits - .filter(a => a.category === 'opportunity') - .sort((a, b) => (b.savings_ms ?? 0) - (a.savings_ms ?? 0)) - const diagnostics = audits.filter(a => a.category === 'diagnostic') + const failingAudits = audits + .filter(a => a.category !== 'passed') + .sort((a, b) => { + // Opportunities first (sorted by savings_ms desc), then diagnostics + if (a.category === 'opportunity' && b.category !== 'opportunity') return -1 + if (a.category !== 'opportunity' && b.category === 'opportunity') return 1 + if (a.category === 'opportunity' && b.category === 'opportunity') { + return (b.savings_ms ?? 0) - (a.savings_ms ?? 0) + } + return 0 + }) const passed = audits.filter(a => a.category === 'passed') // * Core Web Vitals metrics const metrics = [ - { key: 'lcp', label: 'Largest Contentful Paint', value: currentCheck?.lcp_ms ?? null }, - { key: 'cls', label: 'Cumulative Layout Shift', value: currentCheck?.cls ?? null }, - { key: 'tbt', label: 'Total Blocking Time', value: currentCheck?.tbt_ms ?? null }, { key: 'fcp', label: 'First Contentful Paint', value: currentCheck?.fcp_ms ?? null }, + { key: 'lcp', label: 'Largest Contentful Paint', value: currentCheck?.lcp_ms ?? null }, + { key: 'tbt', label: 'Total Blocking Time', value: currentCheck?.tbt_ms ?? null }, + { key: 'cls', label: 'Cumulative Layout Shift', value: currentCheck?.cls ?? null }, { key: 'si', label: 'Speed Index', value: currentCheck?.si_ms ?? null }, { key: 'tti', label: 'Time to Interactive', value: currentCheck?.tti_ms ?? null }, ] + // * Compact score helper for the hero section + const compactScores = [ + { label: 'Accessibility', score: currentCheck?.accessibility_score ?? null }, + { label: 'Best Practices', score: currentCheck?.best_practices_score ?? null }, + { label: 'SEO', score: currentCheck?.seo_score ?? null }, + ] + + function getScoreColor(score: number | null): string { + if (score === null) return '#6b7280' + if (score >= 90) return '#0cce6b' + if (score >= 50) return '#ffa400' + return '#ff4e42' + } + + function getMetricDotColor(metric: string, value: number | null): string { + if (value === null) return 'bg-neutral-400' + const status = getMetricStatus(metric, value) + if (status.label === 'Good') return 'bg-emerald-500' + if (status.label === 'Needs Improvement') return 'bg-amber-500' + return 'bg-red-500' + } + // * Enabled state — show full PageSpeed dashboard return (
@@ -305,81 +334,101 @@ export default function PageSpeedPage() {
- {/* Section 1 — Score Overview */} -
- {/* Score gauges */} -
-
-
- + {/* Section 1 — Hero Card: Score Gauge + Compact Scores + Screenshot */} +
+
+ {/* Left — Large Performance Gauge */} +
+ +
+ + {/* Center — Compact Scores + Meta */} +
+
+ {compactScores.map(({ label, score }) => ( +
+ + + {score !== null ? Math.round(score) : '--'} + + + {label} + +
+ ))}
-
- + + {/* Last checked + frequency */} +
+ {currentCheck?.checked_at && ( + Last checked {formatTimeAgo(currentCheck.checked_at)} + )} + {config?.frequency && ( + + {config.frequency} + + )}
-
- -
-
- + + {/* Score Legend */} +
+ + + 0–49 Poor + + + + 50–89 Needs Improvement + + + + 90–100 Good +
+ + {/* Right — Screenshot */} + {currentCheck?.screenshot && ( +
+ {`${strategy} +
+ )}
- {/* Screenshot */} - {currentCheck?.screenshot && ( -
- {`${strategy} -
- )}
- {/* Last checked info */} -
- {currentCheck?.checked_at && ( - Last checked {formatTimeAgo(currentCheck.checked_at)} - )} - {config?.frequency && ( - - {config.frequency} - - )} -
- - {/* Section 2 — Core Web Vitals */} -
-

- Core Web Vitals + {/* Section 2 — Metrics Card */} +
+

+ Metrics

-
- {metrics.map(({ key, label, value }) => { - const status = getMetricStatus(key, value) - return ( -
-
+
+ {metrics.map(({ key, label, value }) => ( +
+ +
+
{label}
-
+
{formatMetricValue(key, value)}
- - {status.label} -
- ) - })} +
+ ))}
{/* Section 3 — Score Trend Chart */} {chartData.length >= 2 && ( -
-

+
+

Performance Score Trend

@@ -434,73 +483,74 @@ export default function PageSpeedPage() {
)} - {/* Section 4 — Diagnostics Accordion */} + {/* Section 4 — Diagnostics */} {audits.length > 0 && ( -
+

Diagnostics

-
- {/* Opportunities */} - {opportunities.length > 0 && ( -
- - Opportunities ({opportunities.length}) - -
- {opportunities.map(audit => )} -
-
- )} - {/* Diagnostics */} - {diagnostics.length > 0 && ( -
- - Diagnostics ({diagnostics.length}) - -
- {diagnostics.map(audit => )} -
-
- )} + {/* Failing audits — flat list sorted by impact */} + {failingAudits.length > 0 && ( +
+ {failingAudits.map(audit => )} +
+ )} - {/* Passed Audits */} - {passed.length > 0 && ( -
- - Passed Audits ({passed.length}) - -
- {passed.map(audit => )} -
-
- )} -
+ {/* Passed audits — collapsed */} + {passed.length > 0 && ( +
+ + {passed.length} passed audit{passed.length !== 1 ? 's' : ''} + +
+ {passed.map(audit => )} +
+
+ )}
)}
) } +// * Severity indicator based on audit score (pagespeed.web.dev style) +function AuditSeverityIcon({ score }: { score: number | null }) { + if (score === null || score < 0.5) { + // Red triangle for poor / unknown + return + } + if (score < 0.9) { + // Amber square for needs improvement + return + } + // Green circle for good + return +} + // * Expandable audit row with description and detail items function AuditRow({ audit }: { audit: AuditSummary }) { return (
- -
- {audit.title} + + + {audit.title} {audit.display_value && ( - {audit.display_value} + {audit.display_value} )} - + {audit.savings_ms != null && audit.savings_ms > 0 && !audit.display_value && ( + + {audit.savings_ms < 1000 ? `${Math.round(audit.savings_ms)}ms` : `${(audit.savings_ms / 1000).toFixed(1)}s`} + + )} + -
+
{/* Description */} {audit.description && ( -

{audit.description}

+

{audit.description}

)} {/* Items table */} {audit.details && Array.isArray(audit.details) && audit.details.length > 0 && ( diff --git a/components/pagespeed/ScoreGauge.tsx b/components/pagespeed/ScoreGauge.tsx index 78886ca..7b9ceb4 100644 --- a/components/pagespeed/ScoreGauge.tsx +++ b/components/pagespeed/ScoreGauge.tsx @@ -3,6 +3,7 @@ interface ScoreGaugeProps { score: number | null label: string + size?: number } const RADIUS = 44 @@ -14,15 +15,17 @@ function getColor(score: number): string { return '#ff4e42' } -export default function ScoreGauge({ score, label }: ScoreGaugeProps) { +export default function ScoreGauge({ score, label, size = 120 }: 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' + const fontSize = size >= 160 ? 'text-4xl' : 'text-2xl' + return (
-
+
{displayScore !== null ? displayScore : (