Feat: Complete UI implementation for GTmetrix dashboard

This commit is contained in:
2025-12-28 05:09:13 +11:00
parent 3797a45628
commit 7d9c7d9651
2 changed files with 161 additions and 15 deletions

79
main.js
View File

@@ -38,6 +38,7 @@ async function runTest() {
console.log('Run Test triggered'); console.log('Run Test triggered');
const urlInput = document.getElementById('test-url'); const urlInput = document.getElementById('test-url');
const url = urlInput.value.trim(); const url = urlInput.value.trim();
const captureFilmstrip = document.getElementById('capture-filmstrip')?.checked || false;
const errorMsg = document.getElementById('error-msg'); const errorMsg = document.getElementById('error-msg');
const resultsArea = document.getElementById('results-area'); const resultsArea = document.getElementById('results-area');
@@ -67,7 +68,9 @@ async function runTest() {
}, },
body: JSON.stringify({ body: JSON.stringify({
url: url, url: url,
isMobile: currentDevice === 'mobile' url: url,
isMobile: currentDevice === 'mobile',
captureFilmstrip: captureFilmstrip
}) })
}); });
@@ -88,11 +91,53 @@ async function runTest() {
function displayResults(data) { function displayResults(data) {
const resultsArea = document.getElementById('results-area'); const resultsArea = document.getElementById('results-area');
// Update Metrics // Calculate grades using grades.js if available, otherwise simplified logic
updateMetric('score-perf', Math.round(data.scores.performance), true); let overallGrade = 'F';
updateMetric('metric-lcp', Math.round(data.metrics.lcp)); let structureScore = 50;
updateMetric('metric-cls', data.metrics.cls.toFixed(3));
updateMetric('metric-tbt', Math.round(data.metrics.tbt)); if (typeof calculateAllGrades === 'function') {
// Assume grades.js available
const grades = calculateAllGrades(data.metrics);
// Map average score to grade? Simplified:
// Use Performance Score as primary grade driver
}
const perfScore = Math.round(data.scores.performance);
overallGrade = perfScore >= 90 ? 'A' : perfScore >= 80 ? 'B' : perfScore >= 70 ? 'C' : perfScore >= 60 ? 'D' : 'F';
// Structure Score (Average of non-perf categories)
structureScore = Math.round((
(data.scores.seo || 0) +
(data.scores.bestPractices || data.scores['best-practices'] || 0) +
(data.scores.accessibility || 0)
) / 3);
// Update Dashboard UI
const gradeCircle = document.getElementById('overall-grade');
const gradeLetter = gradeCircle.querySelector('.grade-letter');
// Animate Grade
gradeLetter.textContent = overallGrade;
gradeCircle.className = 'grade-circle grade-' + overallGrade.toLowerCase();
document.getElementById('performance-score').textContent = perfScore + '%';
document.getElementById('structure-score').textContent = structureScore + '%';
// Web Vitals
const lcpVal = data.metrics.lcp < 1000 ? (data.metrics.lcp/1000).toFixed(2) + 's' : Math.round(data.metrics.lcp) + 'ms';
const tbtVal = Math.round(data.metrics.tbt) + 'ms';
const clsVal = data.metrics.cls.toFixed(2);
document.getElementById('vital-lcp').textContent = lcpVal;
document.getElementById('vital-tbt').textContent = tbtVal;
document.getElementById('vital-cls').textContent = clsVal;
// Display Filmstrip
if (data.filmstrip && data.filmstrip.length > 0) {
displayFilmstrip(data.filmstrip);
} else {
document.getElementById('filmstrip-section').style.display = 'none';
}
// Remove existing actions if any // Remove existing actions if any
const existingActions = resultsArea.querySelector('.report-actions'); const existingActions = resultsArea.querySelector('.report-actions');
@@ -141,16 +186,20 @@ function displayResults(data) {
resultsArea.scrollIntoView({ behavior: 'smooth' }); resultsArea.scrollIntoView({ behavior: 'smooth' });
} }
function updateMetric(id, value, isScore = false) { function displayFilmstrip(items) {
const el = document.getElementById(id); const section = document.getElementById('filmstrip-section');
el.textContent = value; const container = document.getElementById('filmstrip-container');
section.style.display = 'block';
if (isScore) { // Filter/Sample items if too many
el.className = 'metric-value'; // Reset const frames = items;
if (value >= 90) el.classList.add('score-good');
else if (value >= 50) el.classList.add('score-average'); container.innerHTML = frames.map(frame => `
else el.classList.add('score-poor'); <div class="filmstrip-frame">
} <img src="${frame.data}" alt="Timestamp: ${frame.timing}ms">
<div class="frame-time">${(frame.timing / 1000).toFixed(1)}s</div>
</div>
`).join('');
} }
function showError(msg) { function showError(msg) {

View File

@@ -116,6 +116,103 @@ body::before {
z-index: 0; z-index: 0;
} }
/* =========================================
GTmetrix Dashboard
========================================= */
.gtmetrix-dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.dashboard-card {
background: var(--color-bg-tertiary); /* Darker internal card */
padding: 1.5rem;
border-radius: 12px;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 180px;
}
.grade-card h3 { margin-bottom: 1rem; color: var(--color-text-secondary); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px; }
.grade-circle {
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3.5rem;
font-weight: 800;
color: #fff;
background: #444;
transition: all 0.5s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
.grade-a { background: #0cce6b; box-shadow: 0 0 20px rgba(12, 206, 107, 0.4); }
.grade-b { background: #0ba759; }
.grade-c { background: #ffaa00; }
.grade-d { background: #ff4e00; }
.grade-f { background: #ff0000; }
.scores-card {
flex-direction: row;
gap: 2rem;
}
.score-item { text-align: center; }
.score-label { font-size: 0.9rem; color: var(--color-text-secondary); margin-bottom: 0.5rem; }
.score-value { font-size: 2.5rem; font-weight: 700; color: var(--color-text-primary); }
.score-divider { width: 1px; height: 60px; background: var(--color-border); }
.vitals-card { align-items: stretch; }
.vitals-card h3 { text-align: center; margin-bottom: 1rem; font-size: 0.9rem; color: var(--color-text-secondary); text-transform: uppercase; }
.vitals-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
text-align: center;
}
.vital-label { font-size: 0.8rem; font-weight: 600; margin-bottom: 0.25rem; }
.vital-value { font-size: 1.2rem; font-weight: 500; color: var(--color-accent); }
/* Filmstrip */
.filmstrip-container {
display: flex;
overflow-x: auto;
gap: 0.5rem;
padding: 1rem 0;
scrollbar-width: thin;
}
.filmstrip-frame {
flex: 0 0 auto;
width: 100px;
text-align: center;
}
.filmstrip-frame img {
width: 100%;
height: auto;
border: 1px solid var(--color-border);
margin-bottom: 0.25rem;
}
.frame-time { font-size: 0.75rem; color: var(--color-text-secondary); }
/* Checkbox */
.checkbox-container {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.9rem;
}
.checkbox-container input { width: 16px; height: 16px; }
/* =================================== /* ===================================
LAYOUT STRUCTURE LAYOUT STRUCTURE
=================================== */ =================================== */