503 lines
29 KiB
TypeScript
503 lines
29 KiB
TypeScript
'use client';
|
||
import React, { useState } from 'react';
|
||
import { Treemap, ResponsiveContainer, Tooltip } from 'recharts';
|
||
import IconDesktop from '@/components/icon/icon-desktop';
|
||
import IconRefresh from '@/components/icon/icon-refresh';
|
||
import IconInfoCircle from '@/components/icon/icon-info-circle';
|
||
|
||
interface ReportMobileDesktopProps {
|
||
reports: {
|
||
mobile: { report: any };
|
||
desktop: { report: any };
|
||
} | null;
|
||
loading: boolean;
|
||
}
|
||
|
||
const CustomizedContent = (props: any) => {
|
||
const { root, depth, x, y, width, height, index, name, colors, bg, resourceSize } = props;
|
||
|
||
return (
|
||
<g>
|
||
<rect
|
||
x={x}
|
||
y={y}
|
||
width={width}
|
||
height={height}
|
||
style={{
|
||
fill: bg || '#3b82f633',
|
||
stroke: '#fff',
|
||
strokeWidth: 2 / (depth + 1),
|
||
strokeOpacity: 1 / (depth + 1),
|
||
}}
|
||
/>
|
||
{width > 50 && height > 30 && (
|
||
<text
|
||
x={x + width / 2}
|
||
y={y + height / 2}
|
||
textAnchor="middle"
|
||
fill="#fff"
|
||
fontSize={12}
|
||
fontFamily="inherit"
|
||
fontWeight="bold"
|
||
>
|
||
{name.split('/').pop().split('?')[0].substring(0, 15)}
|
||
</text>
|
||
)}
|
||
</g>
|
||
);
|
||
};
|
||
|
||
const ReportMobileDesktop: React.FC<ReportMobileDesktopProps> = ({ reports, loading }) => {
|
||
const [activeTab, setActiveTab] = useState<'mobile' | 'desktop'>('mobile');
|
||
const getScoreColor = (score: number) => {
|
||
if (score >= 90) return '#10b981'; // Green-500
|
||
if (score >= 50) return '#f59e0b'; // Amber-500
|
||
return '#ef4444'; // Red-500
|
||
};
|
||
|
||
const getScoreLabel = (score: number) => {
|
||
if (score >= 90) return 'Exceptional';
|
||
if (score >= 50) return 'Needs Improvement';
|
||
return 'Critical';
|
||
};
|
||
|
||
const [hoveredMetric, setHoveredMetric] = useState<string | null>(null);
|
||
|
||
const currentReport = reports?.[activeTab]?.report || null;
|
||
|
||
// Format data for Recharts Treemap
|
||
const formatTreemapData = (data: any[]) => {
|
||
if (!data || !Array.isArray(data)) return [];
|
||
return data
|
||
.filter(node => node.resourceSize > 1000) // Only show relevant scripts > 1KB
|
||
.map((node, i) => ({
|
||
name: node.name || 'Unknown',
|
||
size: node.resourceSize || 0,
|
||
bg: i % 2 === 0 ? '#3b82f6' : '#2563eb', // Alternating blues
|
||
resourceSize: (node.resourceSize / 1024).toFixed(1) + ' KB'
|
||
}))
|
||
.sort((a, b) => b.size - a.size)
|
||
.slice(0, 15); // Top 15 scripts
|
||
};
|
||
|
||
const treemapData = formatTreemapData(currentReport?.treemapData);
|
||
|
||
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="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
|
||
stroke={color}
|
||
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>
|
||
|
||
{/* 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>
|
||
) : (
|
||
<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={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>
|
||
|
||
<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>
|
||
);
|
||
};
|
||
|
||
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>
|
||
);
|
||
};
|
||
|
||
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";
|
||
}
|
||
|
||
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>
|
||
);
|
||
};
|
||
|
||
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="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>
|
||
|
||
{/* 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 Inline Visualization */}
|
||
{treemapData && treemapData.length > 0 && (
|
||
<div className="space-y-8 pt-10 border-t dark:border-gray-800">
|
||
<div className="flex flex-col items-center text-center space-y-2">
|
||
<div className="flex items-center gap-2 px-2 text-[10px] font-black text-primary tracking-widest uppercase">
|
||
<span>Resource Map</span>
|
||
<div className="w-1 h-1 bg-primary rounded-full"></div>
|
||
<span>Javascript Distribution</span>
|
||
</div>
|
||
<h3 className="text-3xl font-black text-gray-800 dark:text-white uppercase tracking-tighter">Script Treemap Analysis</h3>
|
||
<p className="text-gray-500 max-w-xl text-sm font-medium">Visual breakdown of your top 15 scripts by resource size. Identifying large third-party scripts is key to performance.</p>
|
||
</div>
|
||
|
||
<div className="panel bg-white dark:bg-black border border-gray-100 dark:border-gray-800 rounded-[40px] p-8 shadow-xl overflow-hidden">
|
||
<div className="h-[400px] w-full">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<Treemap
|
||
data={treemapData}
|
||
dataKey="size"
|
||
aspectRatio={4 / 3}
|
||
stroke="#fff"
|
||
fill="#3b82f6"
|
||
content={<CustomizedContent />}
|
||
>
|
||
<Tooltip
|
||
content={({ active, payload }) => {
|
||
if (active && payload && payload.length) {
|
||
const data = payload[0].payload;
|
||
return (
|
||
<div className="bg-black/90 backdrop-blur-xl border border-white/20 p-4 rounded-2xl shadow-2xl text-white">
|
||
<p className="text-[10px] font-black uppercase text-primary tracking-widest mb-1">Resource Info</p>
|
||
<p className="font-bold text-sm mb-2 max-w-[200px] truncate">{data.name}</p>
|
||
<div className="flex items-center gap-4">
|
||
<div>
|
||
<p className="text-[10px] text-gray-400 uppercase font-bold">Total Size</p>
|
||
<p className="text-lg font-black text-green-400">{data.resourceSize}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
return null;
|
||
}}
|
||
/>
|
||
</Treemap>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ReportMobileDesktop;
|