244 lines
11 KiB
TypeScript
244 lines
11 KiB
TypeScript
'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';
|
|
|
|
interface ReportMobileDesktopProps {
|
|
reports: {
|
|
mobile: { report: any };
|
|
desktop: { report: any };
|
|
};
|
|
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 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 renderScoreCircle = (score: number) => {
|
|
const radius = 45; // bigger circle
|
|
const strokeWidth = 10;
|
|
const circumference = 2 * Math.PI * radius;
|
|
const progress = (score / 100) * circumference;
|
|
|
|
const color = score >= 90 ? "#22c55e" : score >= 50 ? "#eab308" : "#ef4444"; // green, yellow, red
|
|
const lightColor = score >= 90 ? "#bbf7d0" : score >= 50 ? "#fef9c3" : "#fecaca";
|
|
|
|
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 */}
|
|
<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}
|
|
strokeLinecap="round"
|
|
/>
|
|
</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>
|
|
</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;
|
|
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>
|
|
);
|
|
})}
|
|
</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 >
|
|
);
|
|
};
|
|
|
|
export default ReportMobileDesktop;
|