CrawlerX-frontend/components/pages/components-pages-faq-with-tabs.tsx
2025-12-22 22:51:18 +05:30

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;