Add page speed test page and a detailed performance report visualization component.

This commit is contained in:
Alaguraj0361 2026-01-06 17:28:17 +05:30
parent dba51ed30b
commit 8d5b4798c7
2 changed files with 427 additions and 238 deletions

View File

@ -38,12 +38,12 @@ const PageSpeedTest = () => {
setReports(null);
try {
const { data } = await axios.post('https://api.crawlerx.co/api/lighthouse/audit', { url });
const { data } = await axios.post('http://localhost:3020/api/lighthouse/audit', { url });
// normalize the results
const normalizedResults: any = {};
['mobile', 'desktop'].forEach((tab) => {
const tabData = data.results?.[tab] || {};
const tabData: any = data.results?.[tab] || {};
normalizedResults[tab] = {
report: {
scores: tabData.scores || {},
@ -51,13 +51,17 @@ const PageSpeedTest = () => {
opportunities: Array.isArray(tabData.opportunities) ? tabData.opportunities : [],
diagnostics: tabData.diagnostics || {},
screenshot: tabData.screenshot || '',
thumbnails: tabData.thumbnails || [],
passedAudits: tabData.passedAudits || [],
treemapPath: tabData.treemapPath || null
},
};
});
setReports(normalizedResults);
} catch (err: any) {
setError(err.response?.data?.message || err.message || 'Error');
const msg = err.response?.data?.error || err.response?.data?.message || err.message || 'Error';
setError(`Audit Failed: ${msg}`);
} finally {
setLoading(false);
}
@ -174,29 +178,35 @@ const PageSpeedTest = () => {
</svg>
</div>
<div className="relative">
<div className="flex flex-col items-center justify-center sm:-ms-32 sm:flex-row xl:-ms-60">
<div className="mb-2 flex gap-1 text-end text-base leading-5 sm:flex-col xl:text-xl">
<span>It&apos;s free </span>
<span>For everyone</span>
<div className="mb-12 text-center">
<h1 className="text-4xl md:text-5xl font-black mb-4 dark:text-white">PageSpeed Insights</h1>
<p className="text-gray-500 text-lg">Make your web pages fast on all devices</p>
</div>
<div className="me-4 ms-2 hidden text-[#0E1726] rtl:rotate-y-180 dark:text-white sm:block">
<IconArrowWaveLeftUp className="w-16 xl:w-28" />
</div>
<div className="mb-2 text-center text-2xl font-bold dark:text-white md:text-5xl">PageSpeed Audit</div>
</div>
<p className="mb-9 text-center text-base font-semibold">Optimize your website for faster load times, better SEO, and a seamless user experience.</p>
<form action="" method="" className="mb-6">
<div className="relative mx-auto max-w-[580px]">
<input type="text" value={url}
onChange={(e) => setUrl(e.target.value)} placeholder="Enter a Web Page URL" className="form-input py-3 ltr:pr-[100px] rtl:pl-[100px]" />
<button type="button" onClick={handleAudit}
disabled={loading} className={`btn btn-primary absolute top-1 shadow-none ltr:right-1 rtl:left-1 ${loading ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'
}`}>
{loading ? 'Auditing...' : 'Run Audit'}
<div className="max-w-4xl mx-auto">
<form onSubmit={(e) => { e.preventDefault(); handleAudit(); }} className="relative mb-12">
<div className="flex items-center bg-white dark:bg-gray-900 border-2 border-gray-100 dark:border-gray-800 rounded-lg shadow-xl overflow-hidden focus-within:border-blue-500 transition-all p-2">
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="Enter a web page URL"
className="flex-1 bg-transparent border-none focus:ring-0 py-4 px-6 text-lg text-gray-700 dark:text-gray-200"
/>
<button
type="button"
onClick={handleAudit}
disabled={loading}
className={`px-10 py-4 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-md transition-all flex items-center gap-2 ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{loading ? (
<span className="animate-spin h-5 w-5 border-2 border-white/30 border-t-white rounded-full"></span>
) : 'Analyze'}
</button>
{error && <p className="text-red-500 mt-2 text-center">{error}</p>}
</div>
{error && <p className="absolute -bottom-8 left-0 right-0 text-red-500 text-center font-bold">{error}</p>}
</form>
</div>
<div className="flex flex-wrap items-center justify-center gap-2 font-semibold text-[#2196F3] sm:gap-5">
<div className="whitespace-nowrap font-medium text-black dark:text-white">Popular topics :</div>
<div className="flex items-center justify-center gap-2 sm:gap-5">
@ -221,7 +231,7 @@ const PageSpeedTest = () => {
</div>
</div>
<ComponentsPagesFaqWithTabs reports={reports} loading={loading}/>
<ComponentsPagesFaqWithTabs reports={reports} loading={loading} />
{/* <div className="panel mt-10 text-center md:mt-20">
<h3 className="mb-2 text-xl font-bold dark:text-white md:text-2xl">Still need help?</h3>

View File

@ -1,242 +1,421 @@
'use client';
import React, { useState } from 'react';
import { Treemap, Tooltip as ReTooltip, ResponsiveContainer } from 'recharts';
import IconDesktop from '@/components/icon/icon-desktop';
import IconUser from '@/components/icon/icon-user';
import IconRefresh from '@/components/icon/icon-refresh';
import IconInfoCircle from '@/components/icon/icon-info-circle';
interface ReportMobileDesktopProps {
reports: {
mobile: { report: any };
desktop: { report: any };
};
loading: boolean
} | null;
loading: boolean;
}
const ReportMobileDesktop: React.FC<ReportMobileDesktopProps> = ({ reports, loading }) => {
const [activeTab, setActiveTab] = useState<'mobile' | 'desktop'>('mobile');
const currentReport = reports?.[activeTab]?.report || {
scores: {},
metrics: {},
opportunities: [],
diagnostics: {},
screenshot: '',
const currentReport = reports?.[activeTab]?.report || null;
const getScoreColor = (score: number) => {
if (score >= 90) return '#10b981'; // Green-500
if (score >= 50) return '#f59e0b'; // Amber-500
return '#ef4444'; // Red-500
};
const parseSavings = (value: string): number => {
if (!value) return 0;
const val = parseFloat(value);
if (value.toLowerCase().includes('ms')) return val;
if (value.toLowerCase().includes('s')) return val * 1000;
return val;
const getScoreLabel = (score: number) => {
if (score >= 90) return 'Exceptional';
if (score >= 50) return 'Needs Improvement';
return 'Critical';
};
const renderScoreCircle = (score: number) => {
const radius = 45; // bigger circle
const strokeWidth = 10;
const circumference = 2 * Math.PI * radius;
const progress = (score / 100) * circumference;
const [hoveredMetric, setHoveredMetric] = useState<string | null>(null);
const color = score >= 90 ? "#22c55e" : score >= 50 ? "#eab308" : "#ef4444"; // green, yellow, red
const lightColor = score >= 90 ? "#bbf7d0" : score >= 50 ? "#fef9c3" : "#fecaca";
const getMetricColor = (label: string, value: string) => {
const num = parseFloat(value || "0");
if (label === 'FCP') return num <= 1.8 ? '#10b981' : num <= 3.0 ? '#f59e0b' : '#ef4444';
if (label === 'LCP') return num <= 2.5 ? '#10b981' : num <= 4.0 ? '#f59e0b' : '#ef4444';
if (label === 'TBT') return num <= 200 ? '#10b981' : num <= 600 ? '#f59e0b' : '#ef4444';
if (label === 'CLS') return num <= 0.1 ? '#10b981' : num <= 0.25 ? '#f59e0b' : '#ef4444';
if (label === 'SI') return num <= 3.4 ? '#10b981' : num <= 5.8 ? '#f59e0b' : '#ef4444';
return '#e5e7eb';
};
const renderScoreCircleLarge = (score: number, metrics: any) => {
const radius = 100;
const stroke = 12;
const innerRadius = radius - stroke;
const circumference = 2 * Math.PI * innerRadius;
const mainColor = getScoreColor(score);
// Weights: FCP (10%), SI (10%), LCP (25%), TBT (30%), CLS (25%)
const segments = [
{ id: 'FCP', label: 'FCP', weight: 0.10, value: metrics?.firstContentfulPaint },
{ id: 'LCP', label: 'LCP', weight: 0.25, value: metrics?.largestContentfulPaint },
{ id: 'TBT', label: 'TBT', weight: 0.30, value: metrics?.totalBlockingTime },
{ id: 'CLS', label: 'CLS', weight: 0.25, value: metrics?.cumulativeLayoutShift },
{ id: 'SI', label: 'SI', weight: 0.10, value: metrics?.speedIndex },
];
let currentOffset = 0;
const gap = 4; // Degrees of gap between segments
return (
<div className="relative w-28 h-28 flex items-center justify-center">
<svg className="transform -rotate-90" width="112" height="112">
{/* background light circle */}
<div className="flex flex-col items-center">
<div className="relative inline-flex items-center justify-center">
{/* Background glow */}
<div className="absolute inset-0 blur-[100px] opacity-30 rounded-full transition-all duration-1000" style={{ backgroundColor: mainColor }}></div>
<svg height={radius * 2} width={radius * 2} className="relative transform -rotate-90">
{segments.map((seg, idx) => {
const segmentLength = (seg.weight * circumference) - (gap * (circumference / 360));
const dashArray = `${segmentLength} ${circumference - segmentLength}`;
const dashOffset = -currentOffset;
const color = getMetricColor(seg.id, seg.value);
// Calculate position for labels
const angle = (currentOffset / circumference) * 360 + (seg.weight * 180);
const labelRadius = radius + 25;
const lx = radius + labelRadius * Math.cos((angle * Math.PI) / 180);
const ly = radius + labelRadius * Math.sin((angle * Math.PI) / 180);
currentOffset += (seg.weight * circumference);
return (
<g
key={seg.id}
onMouseEnter={() => setHoveredMetric(seg.label)}
onMouseLeave={() => setHoveredMetric(null)}
className="cursor-pointer transition-all duration-300"
>
{/* Track */}
<circle
cx="56"
cy="56"
r={radius}
stroke={lightColor}
strokeWidth={strokeWidth}
fill="none"
/>
{/* progress circle */}
<circle
cx="56"
cy="56"
r={radius}
stroke={color}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={circumference - progress}
strokeOpacity={0.15}
fill="transparent"
strokeWidth={stroke}
strokeDasharray={dashArray}
strokeDashoffset={dashOffset}
r={innerRadius}
cx={radius}
cy={radius}
/>
{/* Progress */}
<circle
stroke={color}
fill="transparent"
strokeWidth={stroke}
strokeDasharray={dashArray}
strokeDashoffset={dashOffset}
strokeLinecap="round"
className={`transition-all duration-500 ${hoveredMetric === seg.label ? 'stroke-[16px]' : 'stroke-[12px]'}`}
r={innerRadius}
cx={radius}
cy={radius}
/>
</g>
);
})}
</svg>
<span className="absolute text-lg font-bold text-gray-800 dark:text-white">
{score}%
</span>
</div>
);
};
const renderMetrics = (metrics: Record<string, any> = {}) => (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 mb-6">
{Object.entries(metrics).map(([key, value]) => (
<div key={key} className="bg-white dark:bg-[#1B2E4B] p-4 rounded-lg shadow text-center">
<p className="text-sm font-medium text-gray-600 dark:text-gray-300">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="mt-2 font-semibold text-gray-800 dark:text-white">{value ?? '-'}</p>
</div>
))}
</div>
);
const renderOpportunities = (opportunities: any[] = []) => (
<div className="bg-white dark:bg-[#1B2E4B] rounded-lg shadow p-4 mb-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">Opportunities</h3>
{opportunities.length === 0 && <p className="text-gray-500 dark:text-gray-300">No suggestions available.</p>}
{opportunities.map((op, idx) => (
<div key={idx} className="mb-3 border-b last:border-b-0 pb-2 border-gray-200 dark:border-gray-600">
<p className="font-medium text-gray-800 dark:text-white">{op.title ?? '-'}</p>
<p className="text-gray-500 text-sm dark:text-gray-300">{op.description ?? '-'}</p>
{op.estimatedSavings && (
<p className="text-xs text-blue-600 dark:text-blue-400">Estimated Savings: {op.estimatedSavings}</p>
)}
</div>
))}
</div>
);
const renderTreeMap = (opportunities: any[] = []) => {
if (opportunities.length === 0) return null;
const data = opportunities.map(op => ({ name: op.title || 'Untitled', size: parseSavings(op.estimatedSavings) }));
return (
<div className="bg-white dark:bg-[#1B2E4B] rounded-lg shadow p-4 mb-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">Opportunities Tree Map</h3>
<div style={{ width: '100%', height: 400 }}>
<ResponsiveContainer>
<Treemap data={data} dataKey="size" nameKey="name" stroke="#fff" fill="#3182ce">
<ReTooltip content={({ payload }) => {
if (!payload || payload.length === 0) return null;
const item = payload[0].payload;
return (
<div className="bg-white dark:bg-[#1B2E4B] text-gray-800 dark:text-white p-2 rounded shadow">
<p className="font-semibold">{item.name}</p>
<p>{item.size} ms estimated savings</p>
</div>
);
}} />
</Treemap>
</ResponsiveContainer>
{/* Central Score */}
<div className="absolute inset-0 flex flex-col items-center justify-center text-center">
{hoveredMetric ? (
<div className="animate-in fade-in zoom-in duration-300">
<span className="text-sm font-black text-gray-400 uppercase tracking-[0.2em]">{hoveredMetric}</span>
<div className="text-4xl font-black tabular-nums" style={{
color: getMetricColor(hoveredMetric === 'FCP' ? 'FCP' : hoveredMetric === 'LCP' ? 'LCP' : hoveredMetric === 'TBT' ? 'TBT' : hoveredMetric === 'CLS' ? 'CLS' : 'SI',
segments.find(s => s.label === hoveredMetric)?.value)
}}>
{segments.find(s => s.label === hoveredMetric)?.value?.split(' ')[0]}
</div>
</div>
);
};
const renderDiagnostics = (diagnostics: any = {}) => (
<div className="bg-white dark:bg-[#1B2E4B] rounded-lg shadow p-4 mb-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-white">Diagnostics</h3>
{Object.keys(diagnostics).length === 0 && <p className="text-gray-500 dark:text-gray-300">No diagnostics available.</p>}
<div className="divide-y divide-gray-200 dark:divide-gray-600">
{Object.entries(diagnostics).map(([key, value]) => (
<div key={key} className="py-3 flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
{key === 'http2' && (
<p className="text-xs text-gray-500 dark:text-gray-400">
All resources should be served via HTTP/2 for better performance.
</p>
)}
</div>
<span className={`px-2 py-1 text-xs font-bold rounded ${value ? 'bg-green-100 text-green-700 dark:bg-green-800 dark:text-green-300' : 'bg-red-100 text-red-600 dark:bg-red-800 dark:text-red-300'}`}>
{value ? 'Pass' : 'Fail'}
</span>
</div>
))}
</div>
</div>
);
return (
<div>
{/* Tabs with your flex-col style */}
<div className="mb-8 flex justify-center gap-4 rounded-md bg-[#DBE7FF] dark:bg-[#141F31] p-3">
<button
className={`flex items-center flex-col gap-2 px-6 py-3 font-bold rounded-md transition-colors duration-300 ${activeTab === 'mobile'
? 'bg-white text-[#2196F3] dark:bg-[#1B2E4B] dark:text-[#2196F3]'
: 'text-[#506690] hover:bg-white hover:text-[#2196F3] dark:text-[#9aa3b1] dark:hover:bg-[#1B2E4B] dark:hover:text-[#2196F3]'
}`}
onClick={() => setActiveTab('mobile')}
>
<IconDesktop fill={true} /> Mobile
</button>
<button
className={`flex items-center flex-col gap-2 px-6 py-3 font-bold rounded-md transition-colors duration-300 ${activeTab === 'desktop'
? 'bg-white text-[#2196F3] dark:bg-[#1B2E4B] dark:text-[#2196F3]'
: 'text-[#506690] hover:bg-white hover:text-[#2196F3] dark:text-[#9aa3b1] dark:hover:bg-[#1B2E4B] dark:hover:text-[#2196F3]'
}`}
onClick={() => setActiveTab('desktop')}
>
<IconDesktop fill={true} /> Desktop
</button>
</div>
{
loading ? (
<div className='panel p-5 h-[300px] flex items-center justify-center'>
<img
src="/assets/images/black-logo.png"
alt="Loading..."
className="w-32 mb-6 animate-pulse"
/>
</div>
) : (
< div className="w-full mt-4 border p-5">
{/* Top Scores */}
<h3 className="text-3xl font-semibold mb-5 text-center text-gray-800 dark:text-white">Diagnose performance issues</h3>
<div className="flex flex-wrap gap-6 justify-center mb-6 pb-5 border-b">
{['performance', 'accessibility', 'bestPractices', 'seo', 'pwa'].map((key) => {
const score = currentReport.scores?.[key];
if (score === undefined || score === null) return null;
<div className="animate-in fade-in zoom-in duration-500">
<span className="text-7xl font-black tracking-tighter" style={{ color: mainColor }}>
{score}
</span>
</div>
)}
</div>
{/* Metric Labels (SI, FCP, LCP...) */}
<div className="absolute inset-0 pointer-events-none">
{segments.map((seg, idx) => {
// Manual angle positioning for exact match to image
const angles = { SI: -140, FCP: -50, LCP: 20, TBT: 90, CLS: 180 };
const angle = angles[seg.id as keyof typeof angles];
const labelRadius = radius + 35;
const x = radius + labelRadius * Math.cos((angle * Math.PI) / 180);
const y = radius + labelRadius * Math.sin((angle * Math.PI) / 180);
return (
<div key={key} className="text-center">
{renderScoreCircle(score)}
<p className="text-sm font-medium mt-2">{key.charAt(0).toUpperCase() + key.slice(1)}</p>
<div
key={seg.id}
className={`absolute transition-all duration-300 font-bold text-[10px] tracking-widest ${hoveredMetric === seg.label ? 'text-black dark:text-white scale-125' : 'text-gray-400'}`}
style={{
left: `${(x / (radius * 2)) * 100}%`,
top: `${(y / (radius * 2)) * 100}%`,
transform: 'translate(-50%, -50%)'
}}
>
{seg.label}
</div>
);
})}
</div>
</div>
{/* Metrics */}
{renderMetrics(currentReport.metrics)}
<div className="mt-8 text-center space-y-4">
<h2 className="text-4xl font-black text-gray-800 dark:text-white uppercase tracking-tighter">Performance</h2>
<p className="text-gray-500 text-sm font-medium max-w-xs leading-tight">
Values are estimated and may vary. The <span className="text-primary cursor-help underline decoration-dotted">performance score</span> is calculated directly from these metrics.
</p>
<div className="flex items-center justify-center gap-6 pt-4 text-[10px] font-black uppercase tracking-widest text-gray-400">
<div className="flex items-center gap-2"><div className="w-3 h-3 bg-[#ef4444] rotate-45"></div> 0-49</div>
<div className="flex items-center gap-2"><div className="w-3 h-3 bg-[#f59e0b]"></div> 50-89</div>
<div className="flex items-center gap-2"><div className="w-10 h-3 bg-[#10b981] rounded-full"></div> 90-100</div>
</div>
</div>
</div>
);
};
{/* Opportunities */}
{renderOpportunities(currentReport.opportunities)}
const renderMiniScore = (score: number, title: string) => {
const color = getScoreColor(score);
return (
<div className="flex flex-col items-center gap-3 panel bg-white/50 dark:bg-black/50 backdrop-blur-md border !border-gray-100 dark:!border-gray-800 p-4 transition-transform hover:scale-105">
<div className="w-12 h-12 rounded-full flex items-center justify-center font-black border-4 text-lg" style={{ borderColor: `${color}40`, color, backgroundColor: `${color}10` }}>
{score}
</div>
<span className="text-[10px] font-black uppercase text-gray-500 tracking-widest">{title}</span>
</div>
);
};
{/* Tree Map */}
{renderTreeMap(currentReport.opportunities)}
const renderMetricPSI = (label: string, value: string) => {
let statusColor = "#10b981";
const num = parseFloat(value || "0");
if (label.includes('Paint') || label.includes('Interactive')) {
const limit = label.includes('Largest') ? 2.5 : 1.5;
if (num > limit) statusColor = "#f59e0b";
if (num > limit * 2) statusColor = "#ef4444";
}
{/* Diagnostics */}
{renderDiagnostics(currentReport.diagnostics)}
return (
<div className="relative p-6 rounded-2xl border border-gray-100 dark:border-gray-800 bg-gradient-to-br from-white to-gray-50 dark:from-gray-900 dark:to-black">
<div className="flex items-center gap-3 mb-3">
<div className="w-2 h-6 rounded-full" style={{ backgroundColor: statusColor }}></div>
<span className="text-gray-500 dark:text-gray-400 font-bold uppercase text-[10px] tracking-widest">{label}</span>
</div>
<div className="text-3xl font-black text-black dark:text-white px-5 tabular-nums">{value || '---'}</div>
</div>
);
};
{/* Screenshot */}
{currentReport.screenshot && (
<div className="flex justify-center mb-6">
if (loading) {
return (
<div className="mt-20 flex flex-col items-center justify-center py-20 bg-white dark:bg-black rounded-[40px] shadow-2xl border border-gray-100 dark:border-gray-800">
<div className="relative w-32 h-32">
<div className="absolute inset-0 border-8 border-primary/10 rounded-full"></div>
<div className="absolute inset-0 border-8 border-transparent border-t-primary rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-4xl">🚀</span>
</div>
</div>
<h3 className="mt-10 text-3xl font-black text-gray-800 dark:text-white leading-tight">Quantifying speed...</h3>
<p className="mt-3 text-gray-400 font-medium max-w-sm text-center">Simulated device runs are operating in parallel to provide instant feedback.</p>
<div className="mt-8 flex gap-2">
<div className="w-2 h-2 bg-primary rounded-full animate-bounce [animation-delay:-0.3s]"></div>
<div className="w-2 h-2 bg-primary rounded-full animate-bounce [animation-delay:-0.15s]"></div>
<div className="w-2 h-2 bg-primary rounded-full animate-bounce"></div>
</div>
</div>
);
}
if (!reports) return null;
return (
<div className="mt-16 space-y-16 animate-fade-in">
{/* Environment Control */}
<div className="flex flex-col items-center">
<div className="bg-gray-100 dark:bg-gray-800/50 backdrop-blur-xl p-1.5 rounded-[2rem] flex gap-2 shadow-inner border border-white/10">
<button
onClick={() => setActiveTab('mobile')}
className={`flex items-center gap-3 px-10 py-4 rounded-[1.8rem] font-black transition-all duration-500 ${activeTab === 'mobile' ? 'bg-primary text-white shadow-xl scale-105' : 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}`}
>
<span className="text-xl">📱</span> MOBILE
</button>
<button
onClick={() => setActiveTab('desktop')}
className={`flex items-center gap-3 px-10 py-4 rounded-[1.8rem] font-black transition-all duration-500 ${activeTab === 'desktop' ? 'bg-primary text-white shadow-xl scale-105' : 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}`}
>
<span className="text-xl">💻</span> DESKTOP
</button>
</div>
</div>
{/* Core Section: Large Score and Summary */}
<div className="panel bg-[#f8fafc] dark:bg-[#0b1120] border-none shadow-none rounded-[40px] p-10 lg:p-16">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20 items-center">
<div className="flex flex-col items-center lg:items-start space-y-12">
{renderScoreCircleLarge(currentReport?.scores?.performance, currentReport?.metrics)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
{renderMiniScore(currentReport?.scores?.accessibility, "Accessibility")}
{renderMiniScore(currentReport?.scores?.bestPractices, "Best Practices")}
{renderMiniScore(currentReport?.scores?.seo, "SEO")}
{currentReport?.scores?.pwa !== null && renderMiniScore(currentReport?.scores?.pwa, "PWA")}
</div>
</div>
<div className="space-y-8">
<div className="relative group overflow-hidden rounded-[32px] border-4 border-white dark:border-gray-800 shadow-2xl bg-white dark:bg-black p-4 transition-transform hover:-rotate-1">
<img
src={currentReport.screenshot}
alt={`${activeTab} screenshot`}
className="border shadow rounded max-w-full h-auto"
src={currentReport?.screenshot}
alt="Site Screenshot"
className="w-full h-auto object-contain rounded-2xl grayscale-[20%] group-hover:grayscale-0 transition-all duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
</div>
<div className="flex justify-between items-center px-4">
<div className="text-[10px] font-black text-gray-400 uppercase tracking-[0.3em]">Device: {activeTab.toUpperCase()}</div>
<div className="text-[10px] font-black text-primary uppercase tracking-[0.3em]">Status: AUDIT COMPLETE</div>
</div>
</div>
</div>
</div>
{/* Metrics Section */}
<div className="space-y-8">
<div className="flex items-center gap-4">
<h3 className="text-2xl font-black text-gray-800 dark:text-white uppercase tracking-tighter">Vital Metrics</h3>
<div className="h-px flex-1 bg-gradient-to-r from-gray-200 dark:from-gray-800 to-transparent"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{renderMetricPSI("First Contentful Paint", currentReport?.metrics?.firstContentfulPaint)}
{renderMetricPSI("Speed Index", currentReport?.metrics?.speedIndex)}
{renderMetricPSI("Largest Contentful Paint", currentReport?.metrics?.largestContentfulPaint)}
{renderMetricPSI("Time to Interactive", currentReport?.metrics?.timeToInteractive)}
{renderMetricPSI("Total Blocking Time", currentReport?.metrics?.totalBlockingTime)}
{renderMetricPSI("Cumulative Layout Shift", currentReport?.metrics?.cumulativeLayoutShift)}
</div>
</div>
{/* Timeline Filmstrip */}
{currentReport?.thumbnails?.length > 0 && (
<div className="space-y-6">
<div className="flex items-center gap-2 px-2 text-[10px] font-black text-gray-400 tracking-widest uppercase">
<span>Filmstrip</span>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<span>Visual Loading sequence</span>
</div>
<div className="flex overflow-x-auto pb-4 gap-2 scrollbar-hide">
{currentReport.thumbnails.map((thumb: any, i: number) => (
<div key={i} className="shrink-0 flex flex-col items-center group">
<div className="p-1 rounded-xl bg-gray-100 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-800 group-hover:scale-105 transition-transform duration-300">
<img src={thumb.data} alt={`Frame ${i}`} className="h-28 w-auto rounded-lg shadow-sm bg-white" />
</div>
<span className="text-[10px] font-bold mt-2 text-gray-500">{(thumb.timing / 1000).toFixed(1)}s</span>
</div>
))}
</div>
</div>
)}
{/* Detailed Audits Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 pt-10 border-t dark:border-gray-800">
{/* Opportunities */}
<div className="space-y-8">
<div className="flex items-center gap-3">
<span className="text-2xl"></span>
<h3 className="text-xl font-black uppercase tracking-tighter text-gray-800 dark:text-white">Performance Opportunities</h3>
</div>
<div className="space-y-4">
{currentReport?.opportunities?.length > 0 ? (
currentReport.opportunities.map((op: any, i: number) => (
<div key={i} className="group p-6 rounded-3xl bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 hover:border-amber-200 dark:hover:border-amber-900/50 transition-all shadow-sm hover:shadow-xl">
<div className="flex justify-between items-start mb-2">
<h4 className="font-black text-blue-600 dark:text-blue-400 pr-4">{op.title}</h4>
<span className="px-3 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 text-[10px] font-black rounded-full whitespace-nowrap">-{op.estimatedSavings}</span>
</div>
<p className="text-sm text-gray-500 leading-relaxed font-medium">{op.description.split('[')[0]}</p>
</div>
))
) : (
<div className="p-10 text-center rounded-3xl bg-green-50 dark:bg-green-950/20 text-green-600 font-bold border-2 border-dashed border-green-200 dark:border-green-900/30">
No performance bottlenecks identified!
</div>
)}
</div>
)
}
</div>
</div >
{/* Diagnostics */}
<div className="space-y-8">
<div className="flex items-center gap-3">
<span className="text-2xl">🛡</span>
<h3 className="text-xl font-black uppercase tracking-tighter text-gray-800 dark:text-white">Security & Best Practices</h3>
</div>
<div className="grid grid-cols-1 gap-4">
{Object.entries(currentReport?.diagnostics || {}).map(([key, pass]: any, i: number) => (
<div key={i} className="flex items-center justify-between p-5 rounded-2xl bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 transition-all hover:translate-x-1">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${pass ? 'bg-green-100 dark:bg-green-950 text-green-600' : 'bg-red-100 dark:bg-red-950 text-red-600'}`}>
{pass ? "✓" : "!"}
</div>
<span className="font-bold text-gray-700 dark:text-gray-200 capitalize tracking-tight">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())}
</span>
</div>
<span className={`text-[10px] font-black px-3 py-1 rounded-full uppercase tracking-tighter ${pass ? 'bg-green-50 dark:bg-green-950 text-green-600' : 'bg-red-50 dark:bg-red-950 text-red-600'}`}>
{pass ? "Secure" : "Warning"}
</span>
</div>
))}
</div>
</div>
</div>
{/* Passed Audits Recap */}
<div className="space-y-6">
<div className="flex items-center gap-2 px-2 text-[10px] font-black text-green-500 tracking-widest uppercase">
<span>Passed Checks</span>
<div className="w-1 h-1 bg-green-500 rounded-full"></div>
<span>All successful audits</span>
</div>
<div className="flex flex-wrap gap-2">
{currentReport?.passedAudits?.map((title: string, i: number) => (
<div key={i} className="flex items-center gap-2 px-4 py-2 bg-gray-50 dark:bg-gray-900 border border-gray-100 dark:border-gray-800 rounded-full group hover:border-green-200 transition-colors">
<span className="text-green-500 text-xs"></span>
<span className="text-[11px] font-bold text-gray-500 group-hover:text-green-600 transition-colors">{title}</span>
</div>
))}
</div>
</div>
{/* Treemap Final CTA */}
{currentReport?.treemapPath && (
<div className="relative p-1 overflow-hidden rounded-[40px] bg-gradient-to-r from-primary via-blue-400 to-primary group shadow-2xl">
<div className="relative p-12 bg-white dark:bg-black rounded-[39px] flex flex-col items-center text-center space-y-6 overflow-hidden">
{/* Abstract background shape */}
<div className="absolute top-0 right-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl -mr-48 -mt-48 group-hover:bg-primary/10 transition-colors"></div>
<h4 className="text-4xl font-black tracking-tighter text-black dark:text-white max-w-xl leading-[1.1]">In-Depth Resource Treemap Analysis</h4>
<p className="text-gray-500 font-medium max-w-lg text-lg">Uncover hidden third-party scripts and Bloated assets that are stealing your performance. Interactive visual report awaits.</p>
<a
href={`http://localhost:3020${currentReport.treemapPath}`}
target="_blank"
rel="noreferrer"
className="bg-primary hover:bg-blue-700 text-white px-12 py-5 rounded-[2rem] font-black uppercase tracking-widest shadow-[0_20px_50px_rgba(59,130,246,0.3)] hover:shadow-none transition-all hover:scale-105 active:scale-95"
>
Explore Treemap
</a>
</div>
</div>
)}
</div>
);
};