diff --git a/app/(defaults)/page-speed-test/page.tsx b/app/(defaults)/page-speed-test/page.tsx index 8e2f5ac..9752ccd 100644 --- a/app/(defaults)/page-speed-test/page.tsx +++ b/app/(defaults)/page-speed-test/page.tsx @@ -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 = () => {
-
-
- It's free - For everyone -
-
- -
-
PageSpeed Audit
+
+

PageSpeed Insights

+

Make your web pages fast on all devices

+
+ +
+
{ e.preventDefault(); handleAudit(); }} className="relative mb-12"> +
+ 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" + /> + +
+ {error &&

{error}

} +
-

Optimize your website for faster load times, better SEO, and a seamless user experience.

-
-
- setUrl(e.target.value)} placeholder="Enter a Web Page URL" className="form-input py-3 ltr:pr-[100px] rtl:pl-[100px]" /> - - {error &&

{error}

} -
-
Popular topics :
@@ -220,9 +230,9 @@ const PageSpeedTest = () => {
- - - + + + {/*

Still need help?

diff --git a/components/pages/components-pages-faq-with-tabs.tsx b/components/pages/components-pages-faq-with-tabs.tsx index 71b4213..a8ce34e 100644 --- a/components/pages/components-pages-faq-with-tabs.tsx +++ b/components/pages/components-pages-faq-with-tabs.tsx @@ -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 = ({ 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(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 ( -
- - {/* background light circle */} - - {/* progress circle */} - - - - {score}% - -
- ); - }; +
+
+ {/* Background glow */} +
+ + {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); - const renderMetrics = (metrics: Record = {}) => ( -
- {Object.entries(metrics).map(([key, value]) => ( -
-

- {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} -

-

{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); - const renderOpportunities = (opportunities: any[] = []) => ( -
-

Opportunities

- {opportunities.length === 0 &&

No suggestions available.

} - {opportunities.map((op, idx) => ( -
-

{op.title ?? '-'}

-

{op.description ?? '-'}

- {op.estimatedSavings && ( -

Estimated Savings: {op.estimatedSavings}

- )} -
- ))} -
- ); + currentOffset += (seg.weight * circumference); - const renderTreeMap = (opportunities: any[] = []) => { - if (opportunities.length === 0) return null; + return ( + setHoveredMetric(seg.label)} + onMouseLeave={() => setHoveredMetric(null)} + className="cursor-pointer transition-all duration-300" + > + {/* Track */} + + {/* Progress */} + + + ); + })} + - const data = opportunities.map(op => ({ name: op.title || 'Untitled', size: parseSavings(op.estimatedSavings) })); - - return ( -
-

Opportunities Tree Map

-
- - - { - if (!payload || payload.length === 0) return null; - const item = payload[0].payload; - return ( -
-

{item.name}

-

{item.size} ms estimated savings

-
- ); - }} /> -
-
-
-
- ); - }; - - const renderDiagnostics = (diagnostics: any = {}) => ( -
-

Diagnostics

- {Object.keys(diagnostics).length === 0 &&

No diagnostics available.

} -
- {Object.entries(diagnostics).map(([key, value]) => ( -
-
-

- {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} -

- {key === 'http2' && ( -

- All resources should be served via HTTP/2 for better performance. -

- )} -
- - {value ? 'Pass' : 'Fail'} - -
- ))} -
-
- ); - - return ( -
- {/* Tabs with your flex-col style */} -
- - - -
- - { - loading ? ( -
- Loading... - - -
- - ) : ( - < div className="w-full mt-4 border p-5"> - {/* Top Scores */} -

Diagnose performance issues

-
- {['performance', 'accessibility', 'bestPractices', 'seo', 'pwa'].map((key) => { - const score = currentReport.scores?.[key]; - if (score === undefined || score === null) return null; - return ( -
- {renderScoreCircle(score)} -

{key.charAt(0).toUpperCase() + key.slice(1)}

-
- ); - })} -
- - {/* Metrics */} - {renderMetrics(currentReport.metrics)} - - {/* Opportunities */} - {renderOpportunities(currentReport.opportunities)} - - {/* Tree Map */} - {renderTreeMap(currentReport.opportunities)} - - {/* Diagnostics */} - {renderDiagnostics(currentReport.diagnostics)} - - {/* Screenshot */} - {currentReport.screenshot && ( -
- {`${activeTab} + {/* Central Score */} +
+ {hoveredMetric ? ( +
+ {hoveredMetric} +
s.label === hoveredMetric)?.value) + }}> + {segments.find(s => s.label === hoveredMetric)?.value?.split(' ')[0]} +
+
+ ) : ( +
+ + {score} +
)}
- ) - } -
+ {/* Metric Labels (SI, FCP, LCP...) */} +
+ {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 ( +
+ {seg.label} +
+ ); + })} +
+
+ +
+

Performance

+

+ Values are estimated and may vary. The performance score is calculated directly from these metrics. +

+
+
0-49
+
50-89
+
90-100
+
+
+
+ ); + }; + + const renderMiniScore = (score: number, title: string) => { + const color = getScoreColor(score); + return ( +
+
+ {score} +
+ {title} +
+ ); + }; + + 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 ( +
+
+
+ {label} +
+
{value || '---'}
+
+ ); + }; + + if (loading) { + return ( +
+
+
+
+
+ 🚀 +
+
+

Quantifying speed...

+

Simulated device runs are operating in parallel to provide instant feedback.

+
+
+
+
+
+
+ ); + } + + if (!reports) return null; + + return ( +
+ {/* Environment Control */} +
+
+ + +
+
+ + {/* Core Section: Large Score and Summary */} +
+
+
+ {renderScoreCircleLarge(currentReport?.scores?.performance, currentReport?.metrics)} + +
+ {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")} +
+
+ +
+
+ Site Screenshot +
+
+
+
Device: {activeTab.toUpperCase()}
+
Status: AUDIT COMPLETE
+
+
+
+
+ + {/* Metrics Section */} +
+
+

Vital Metrics

+
+
+
+ {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)} +
+
+ + {/* Timeline Filmstrip */} + {currentReport?.thumbnails?.length > 0 && ( +
+
+ Filmstrip +
+ Visual Loading sequence +
+
+ {currentReport.thumbnails.map((thumb: any, i: number) => ( +
+
+ {`Frame +
+ {(thumb.timing / 1000).toFixed(1)}s +
+ ))} +
+
+ )} + + {/* Detailed Audits Grid */} +
+ {/* Opportunities */} +
+
+ +

Performance Opportunities

+
+
+ {currentReport?.opportunities?.length > 0 ? ( + currentReport.opportunities.map((op: any, i: number) => ( +
+
+

{op.title}

+ -{op.estimatedSavings} +
+

{op.description.split('[')[0]}

+
+ )) + ) : ( +
+ No performance bottlenecks identified! ✨ +
+ )} +
+
+ + {/* Diagnostics */} +
+
+ 🛡️ +

Security & Best Practices

+
+
+ {Object.entries(currentReport?.diagnostics || {}).map(([key, pass]: any, i: number) => ( +
+
+
+ {pass ? "✓" : "!"} +
+ + {key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())} + +
+ + {pass ? "Secure" : "Warning"} + +
+ ))} +
+
+
+ + {/* Passed Audits Recap */} +
+
+ Passed Checks +
+ All successful audits +
+
+ {currentReport?.passedAudits?.map((title: string, i: number) => ( +
+ + {title} +
+ ))} +
+
+ + {/* Treemap Final CTA */} + {currentReport?.treemapPath && ( +
+
+ {/* Abstract background shape */} +
+ +

In-Depth Resource Treemap Analysis

+

Uncover hidden third-party scripts and Bloated assets that are stealing your performance. Interactive visual report awaits.

+ + + Explore Treemap + +
+
+ )} +
); };