2025-09-27 14:00:29 +05:30

268 lines
9.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import axios from 'axios';
import {
Treemap,
Tooltip as ReTooltip,
ResponsiveContainer
} from 'recharts';
export default function Home() {
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [reports, setReports] = useState<any>(null);
const [error, setError] = useState('');
const [activeTab, setActiveTab] = useState<'mobile' | 'desktop'>('mobile');
// --- Helper to parse estimated savings like "1.2 s" or "1200 ms" ---
function 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 handleAudit = async () => {
if (!url) return setError('Enter a URL');
setLoading(true);
setError('');
setReports(null);
try {
const { data } = await axios.post('https://api.crawlerx.co/api/lighthouse/audit', { url });
// normalize the results
const normalizedResults: any = {};
['mobile', 'desktop'].forEach((tab) => {
const tabData = data.results?.[tab] || {};
normalizedResults[tab] = {
report: {
scores: tabData.scores || {},
metrics: tabData.metrics || {},
opportunities: Array.isArray(tabData.opportunities) ? tabData.opportunities : [],
diagnostics: tabData.diagnostics || {},
screenshot: tabData.screenshot || '',
},
};
});
setReports(normalizedResults);
setActiveTab('mobile');
} catch (err: any) {
setError(err.response?.data?.message || err.message || 'Error');
} finally {
setLoading(false);
}
};
const renderScoreCircle = (score: number) => {
const color =
score >= 90 ? 'text-green-500 border-green-500' :
score >= 50 ? 'text-yellow-500 border-yellow-500' :
'text-red-500 border-red-500';
return (
<div className={`w-20 h-20 flex items-center justify-center rounded-full border-4 ${color}`}>
<span className="text-lg font-bold">{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 p-4 rounded-lg shadow text-center">
<p className="text-sm font-medium text-gray-600">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
<p className="mt-2 font-semibold text-gray-800">{value ?? '-'}</p>
</div>
))}
</div>
);
const renderOpportunities = (opportunities: any[] = []) => (
<div className="bg-white rounded-lg shadow p-4 mb-6">
<h3 className="text-lg font-semibold mb-4">Opportunities</h3>
{opportunities.length === 0 && <p className="text-gray-500">No suggestions available.</p>}
{opportunities.map((op, idx) => (
<div key={idx} className="mb-3 border-b last:border-b-0 pb-2">
<p className="font-medium">{op.title ?? '-'}</p>
<p className="text-gray-500 text-sm">{op.description ?? '-'}</p>
{op.estimatedSavings && (
<p className="text-xs text-blue-600">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 rounded-lg shadow p-4 mb-6">
<h3 className="text-lg font-semibold mb-4">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 text-gray-800 p-2 rounded shadow">
<p className="font-semibold">{item.name}</p>
<p>{item.size} ms estimated savings</p>
</div>
);
}}
/>
</Treemap>
</ResponsiveContainer>
</div>
</div>
);
};
const renderDiagnostics = (diagnostics: any = {}) => {
const keys = Object.keys(diagnostics);
return (
<div className="bg-white rounded-lg shadow p-4 mb-6">
<h3 className="text-lg font-semibold mb-4">Diagnostics</h3>
{keys.length === 0 && <p className="text-gray-500">No diagnostics available.</p>}
<div className="divide-y divide-gray-200">
{keys.map((key) => (
<div key={key} className="py-3 flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-700">
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
</p>
{key === 'http2' && (
<p className="text-xs text-gray-500">
All resources should be served via HTTP/2 for better performance.
</p>
)}
</div>
<span
className={`px-2 py-1 text-xs font-bold rounded ${diagnostics[key] ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-600'
}`}
>
{diagnostics[key] ? 'Pass' : 'Fail'}
</span>
</div>
))}
</div>
</div>
);
};
const currentReport = reports?.[activeTab]?.report || {
scores: {},
metrics: {},
opportunities: [],
diagnostics: {},
screenshot: '',
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center p-4 sm:p-8">
<h1 className="text-2xl sm:text-3xl font-bold mb-6 text-gray-800 text-center">
Lighthouse PageSpeed Audit
</h1>
{/* Input */}
<div className="flex flex-col sm:flex-row gap-4 w-full max-w-xl mb-4">
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="Enter URL"
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleAudit}
disabled={loading}
className={`px-4 py-2 rounded-lg font-semibold text-white shadow-md transition ${loading ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'
}`}
>
{loading ? 'Auditing...' : 'Run Audit'}
</button>
</div>
{error && <p className="text-red-500 mt-2 text-center">{error}</p>}
{reports && (
<div className="w-full max-w-6xl mt-8">
{/* Tabs */}
<div className="flex gap-4 mb-4">
<button
className={`px-4 py-2 rounded-t-lg font-semibold ${activeTab === 'mobile' ? 'bg-blue-600 text-white' : 'bg-gray-200'}`}
onClick={() => setActiveTab('mobile')}
>
Mobile
</button>
<button
className={`px-4 py-2 rounded-t-lg font-semibold ${activeTab === 'desktop' ? 'bg-green-600 text-white' : 'bg-gray-200'}`}
onClick={() => setActiveTab('desktop')}
>
Desktop
</button>
</div>
{/* Top Scores */}
<div className="flex flex-wrap gap-6 justify-center mb-6">
{['performance', 'accessibility', 'bestPractices', 'seo', 'pwa'].map((key) => {
const score = currentReport.scores?.[key];
if (score === undefined || score === null) return null;
return (
<div key={key} className="text-center">
<p className="text-sm font-medium">{key.charAt(0).toUpperCase() + key.slice(1)}</p>
{renderScoreCircle(score)}
</div>
);
})}
</div>
{/* Metrics */}
{renderMetrics(currentReport.metrics)}
{/* Opportunities */}
{renderOpportunities(currentReport.opportunities)}
{/* Tree Map */}
{renderTreeMap(currentReport.opportunities)}
{/* Diagnostics */}
{renderDiagnostics(currentReport.diagnostics)}
{/* Screenshot */}
{currentReport.screenshot && (
<div className="flex justify-center mb-6">
<img
src={currentReport.screenshot}
alt={`${activeTab} screenshot`}
className="border shadow rounded max-w-full h-auto"
/>
</div>
)}
</div>
)}
</div>
);
}