chore: cleanup website files
- Remove website/dashboards/ directory - Remove website/aitbc-proxy.conf - Remove website/README.md - Update website documentation files with minimum Python version - Update website assets (CSS, JS) for improved performance - Update docs/ with minimum Python version and updated paths
This commit is contained in:
@@ -21,10 +21,18 @@ body.dark {
|
||||
--global-header-cta-text: #0f172a;
|
||||
}
|
||||
|
||||
body.light {
|
||||
--global-header-bg: rgba(255, 255, 255, 0.97);
|
||||
--global-header-border: rgba(15, 23, 42, 0.08);
|
||||
--global-header-text: #111827;
|
||||
--global-header-muted: #6b7280;
|
||||
--global-header-pill: rgba(37, 99, 235, 0.07);
|
||||
--global-header-pill-hover: rgba(37, 99, 235, 0.15);
|
||||
--global-header-accent: #2563eb;
|
||||
--global-header-cta-text: #fff;
|
||||
}
|
||||
|
||||
.global-header {
|
||||
height: 90px;
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
@@ -35,13 +43,13 @@ body.dark {
|
||||
}
|
||||
|
||||
.global-header__inner {
|
||||
max-width: 1160px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.25rem;
|
||||
height: 100%;
|
||||
padding: 0.85rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -127,10 +135,27 @@ body.dark {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.global-dark-toggle {
|
||||
border: 1px solid var(--global-header-border);
|
||||
background: transparent;
|
||||
color: var(--global-header-text);
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.9rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.global-dark-toggle:hover {
|
||||
border-color: var(--global-header-accent);
|
||||
color: var(--global-header-accent);
|
||||
}
|
||||
|
||||
.global-subnav {
|
||||
max-width: 1160px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.25rem 0.75rem;
|
||||
display: flex;
|
||||
@@ -159,18 +184,10 @@ body.dark {
|
||||
color: var(--global-header-accent);
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.global-header {
|
||||
height: auto;
|
||||
min-height: 90px;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.global-header__inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.global-header__actions {
|
||||
@@ -182,6 +199,7 @@ body.dark {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.global-brand__text span {
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
(function () {
|
||||
// Always enforce dark theme
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.documentElement.classList.add('dark');
|
||||
|
||||
// Clean up any old user preferences
|
||||
if (localStorage.getItem('theme')) localStorage.removeItem('theme');
|
||||
if (localStorage.getItem('exchangeTheme')) localStorage.removeItem('exchangeTheme');
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ key: 'home', label: 'Home', href: '/' },
|
||||
{ key: 'explorer', label: 'Explorer', href: '/explorer/' },
|
||||
@@ -15,6 +7,8 @@
|
||||
{ key: 'docs', label: 'Docs', href: '/docs/index.html' },
|
||||
];
|
||||
|
||||
const CTA = { label: 'Launch Marketplace', href: '/marketplace/' };
|
||||
|
||||
function determineActiveKey(pathname) {
|
||||
if (pathname.startsWith('/explorer')) return 'explorer';
|
||||
if (pathname.startsWith('/marketplace')) return 'marketplace';
|
||||
@@ -42,11 +36,64 @@
|
||||
</div>
|
||||
</a>
|
||||
<nav class="global-nav">${navLinks}</nav>
|
||||
<div class="global-header__actions">
|
||||
<button type="button" class="global-dark-toggle" data-role="global-theme-toggle">
|
||||
<span class="global-dark-toggle__emoji">🌙</span>
|
||||
<span class="global-dark-toggle__text">Dark</span>
|
||||
</button>
|
||||
<a href="${CTA.href}" class="global-nav__cta">${CTA.label}</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
function getCurrentTheme() {
|
||||
if (document.documentElement.hasAttribute('data-theme')) {
|
||||
return document.documentElement.getAttribute('data-theme');
|
||||
}
|
||||
if (document.documentElement.classList.contains('dark')) return 'dark';
|
||||
if (document.body && document.body.classList.contains('light')) return 'light';
|
||||
return 'light';
|
||||
}
|
||||
|
||||
function updateToggleLabel(theme) {
|
||||
const emojiEl = document.querySelector('.global-dark-toggle__emoji');
|
||||
const textEl = document.querySelector('.global-dark-toggle__text');
|
||||
if (!emojiEl || !textEl) return;
|
||||
if (theme === 'dark') {
|
||||
emojiEl.textContent = '🌙';
|
||||
textEl.textContent = 'Dark';
|
||||
} else {
|
||||
emojiEl.textContent = '☀️';
|
||||
textEl.textContent = 'Light';
|
||||
}
|
||||
}
|
||||
|
||||
function bindThemeToggle() {
|
||||
const toggle = document.querySelector('[data-role="global-theme-toggle"]');
|
||||
if (!toggle) return;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
if (typeof window.toggleDarkMode === 'function') {
|
||||
window.toggleDarkMode();
|
||||
} else if (typeof window.toggleTheme === 'function') {
|
||||
window.toggleTheme();
|
||||
} else {
|
||||
const isDark = document.documentElement.classList.toggle('dark');
|
||||
if (isDark) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => updateToggleLabel(getCurrentTheme()), 0);
|
||||
});
|
||||
|
||||
updateToggleLabel(getCurrentTheme());
|
||||
}
|
||||
|
||||
function initHeader() {
|
||||
const activeKey = determineActiveKey(window.location.pathname);
|
||||
const headerHTML = buildHeader(activeKey);
|
||||
@@ -59,6 +106,8 @@
|
||||
} else {
|
||||
document.body.insertAdjacentHTML('afterbegin', headerHTML);
|
||||
}
|
||||
|
||||
bindThemeToggle();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
@@ -37,6 +37,78 @@ document.querySelectorAll('.feature-card, .arch-component').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Dark mode functionality with enhanced persistence and system preference detection
|
||||
function toggleDarkMode() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
// Apply theme immediately
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
|
||||
// Save to localStorage for persistence
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
// Update button display if it exists
|
||||
updateThemeButton(theme);
|
||||
|
||||
// Send analytics event
|
||||
if (window.analytics) {
|
||||
window.analytics.track('theme_changed', { theme });
|
||||
}
|
||||
}
|
||||
|
||||
function updateThemeButton(theme) {
|
||||
const emoji = document.getElementById('darkModeEmoji');
|
||||
const text = document.getElementById('darkModeText');
|
||||
|
||||
if (emoji && text) {
|
||||
if (theme === 'dark') {
|
||||
emoji.textContent = '🌙';
|
||||
text.textContent = 'Dark';
|
||||
} else {
|
||||
emoji.textContent = '☀️';
|
||||
text.textContent = 'Light';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPreferredTheme() {
|
||||
// 1. Check localStorage first (user preference)
|
||||
const saved = localStorage.getItem('theme');
|
||||
if (saved) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
// 2. Check system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
// 3. Default to dark (AITBC brand preference)
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
function initializeTheme() {
|
||||
const theme = getPreferredTheme();
|
||||
setTheme(theme);
|
||||
|
||||
// Listen for system preference changes
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
// Only auto-switch if user hasn't manually set a preference
|
||||
if (!localStorage.getItem('theme')) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme immediately (before DOM loads)
|
||||
initializeTheme();
|
||||
|
||||
// Touch gesture support for mobile navigation
|
||||
class TouchNavigation {
|
||||
constructor() {
|
||||
@@ -46,53 +118,190 @@ class TouchNavigation {
|
||||
this.touchEndY = 0;
|
||||
this.minSwipeDistance = 50;
|
||||
this.maxVerticalDistance = 100;
|
||||
|
||||
this.initializeTouchEvents();
|
||||
|
||||
// Get all major sections for navigation
|
||||
this.sections = ['hero', 'features', 'architecture', 'achievements', 'documentation'];
|
||||
this.currentSectionIndex = 0;
|
||||
|
||||
this.bindEvents();
|
||||
this.setupMobileOptimizations();
|
||||
}
|
||||
|
||||
initializeTouchEvents() {
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
this.touchStartX = e.changedTouches[0].screenX;
|
||||
this.touchStartY = e.changedTouches[0].screenY;
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', (e) => {
|
||||
this.touchEndX = e.changedTouches[0].screenX;
|
||||
this.touchEndY = e.changedTouches[0].screenY;
|
||||
this.handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
bindEvents() {
|
||||
document.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
|
||||
document.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
|
||||
document.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false });
|
||||
}
|
||||
|
||||
handleSwipe() {
|
||||
const xDiff = this.touchEndX - this.touchStartX;
|
||||
const yDiff = Math.abs(this.touchEndY - this.touchStartY);
|
||||
|
||||
// Ensure swipe is mostly horizontal
|
||||
if (yDiff > this.maxVerticalDistance) return;
|
||||
|
||||
if (Math.abs(xDiff) > this.minSwipeDistance) {
|
||||
if (xDiff > 0) {
|
||||
// Swipe Right - potentially open menu or go back
|
||||
this.onSwipeRight();
|
||||
|
||||
handleTouchStart(e) {
|
||||
this.touchStartX = e.touches[0].clientX;
|
||||
this.touchStartY = e.touches[0].clientY;
|
||||
}
|
||||
|
||||
handleTouchMove(e) {
|
||||
// Prevent scrolling when detecting horizontal swipes
|
||||
const touchCurrentX = e.touches[0].clientX;
|
||||
const touchCurrentY = e.touches[0].clientY;
|
||||
const deltaX = Math.abs(touchCurrentX - this.touchStartX);
|
||||
const deltaY = Math.abs(touchCurrentY - this.touchStartY);
|
||||
|
||||
// If horizontal movement is greater than vertical, prevent default scrolling
|
||||
if (deltaX > deltaY && deltaX > 10) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchEnd(e) {
|
||||
this.touchEndX = e.changedTouches[0].clientX;
|
||||
this.touchEndY = e.changedTouches[0].clientY;
|
||||
|
||||
const deltaX = this.touchEndX - this.touchStartX;
|
||||
const deltaY = Math.abs(this.touchEndY - this.touchStartY);
|
||||
|
||||
// Only process swipe if vertical movement is minimal
|
||||
if (deltaY < this.maxVerticalDistance && Math.abs(deltaX) > this.minSwipeDistance) {
|
||||
if (deltaX > 0) {
|
||||
this.swipeRight();
|
||||
} else {
|
||||
// Swipe Left - potentially close menu or go forward
|
||||
this.onSwipeLeft();
|
||||
this.swipeLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSwipeRight() {
|
||||
// Can be implemented if a side menu is added
|
||||
// console.log('Swiped right');
|
||||
|
||||
swipeLeft() {
|
||||
// Navigate to next section
|
||||
const nextIndex = Math.min(this.currentSectionIndex + 1, this.sections.length - 1);
|
||||
this.navigateToSection(nextIndex);
|
||||
}
|
||||
|
||||
onSwipeLeft() {
|
||||
// Can be implemented if a side menu is added
|
||||
// console.log('Swiped left');
|
||||
|
||||
swipeRight() {
|
||||
// Navigate to previous section
|
||||
const prevIndex = Math.max(this.currentSectionIndex - 1, 0);
|
||||
this.navigateToSection(prevIndex);
|
||||
}
|
||||
|
||||
navigateToSection(index) {
|
||||
const sectionId = this.sections[index];
|
||||
const element = document.getElementById(sectionId);
|
||||
|
||||
if (element) {
|
||||
this.currentSectionIndex = index;
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
// Update URL hash without triggering scroll
|
||||
history.replaceState(null, null, `#${sectionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
setupMobileOptimizations() {
|
||||
// Add touch-friendly interactions
|
||||
this.setupTouchButtons();
|
||||
this.setupScrollOptimizations();
|
||||
this.setupMobileMenu();
|
||||
}
|
||||
|
||||
setupTouchButtons() {
|
||||
// Make buttons more touch-friendly
|
||||
const buttons = document.querySelectorAll('button, .cta-button, .nav-button');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('touchstart', () => {
|
||||
button.style.transform = 'scale(0.98)';
|
||||
}, { passive: true });
|
||||
|
||||
button.addEventListener('touchend', () => {
|
||||
button.style.transform = '';
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
setupScrollOptimizations() {
|
||||
// Improve momentum scrolling on iOS
|
||||
if ('webkitOverflowScrolling' in document.body.style) {
|
||||
document.body.style.webkitOverflowScrolling = 'touch';
|
||||
}
|
||||
|
||||
// Add smooth scrolling for anchor links with touch feedback
|
||||
document.querySelectorAll('a[href^="#"]').forEach(link => {
|
||||
link.addEventListener('touchstart', () => {
|
||||
link.style.opacity = '0.7';
|
||||
}, { passive: true });
|
||||
|
||||
link.addEventListener('touchend', () => {
|
||||
link.style.opacity = '';
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
setupMobileMenu() {
|
||||
// Create mobile menu toggle if nav is hidden on mobile
|
||||
const nav = document.querySelector('nav');
|
||||
if (nav && window.innerWidth < 768) {
|
||||
this.createMobileMenu();
|
||||
}
|
||||
}
|
||||
|
||||
createMobileMenu() {
|
||||
// Create hamburger menu for mobile
|
||||
const header = document.querySelector('header');
|
||||
if (!header) return;
|
||||
|
||||
const mobileMenuBtn = document.createElement('button');
|
||||
mobileMenuBtn.className = 'mobile-menu-btn';
|
||||
mobileMenuBtn.innerHTML = '☰';
|
||||
mobileMenuBtn.setAttribute('aria-label', 'Toggle mobile menu');
|
||||
|
||||
const nav = document.querySelector('nav');
|
||||
if (nav) {
|
||||
nav.style.display = 'none';
|
||||
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
const isOpen = nav.style.display !== 'none';
|
||||
nav.style.display = isOpen ? 'none' : 'flex';
|
||||
mobileMenuBtn.innerHTML = isOpen ? '☰' : '✕';
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!header.contains(e.target)) {
|
||||
nav.style.display = 'none';
|
||||
mobileMenuBtn.innerHTML = '☰';
|
||||
}
|
||||
});
|
||||
|
||||
header.appendChild(mobileMenuBtn);
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentSection() {
|
||||
// Update current section based on scroll position
|
||||
const scrollY = window.scrollY + window.innerHeight / 2;
|
||||
|
||||
this.sections.forEach((sectionId, index) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const elementTop = rect.top + window.scrollY;
|
||||
const elementBottom = elementTop + rect.height;
|
||||
|
||||
if (scrollY >= elementTop && scrollY < elementBottom) {
|
||||
this.currentSectionIndex = index;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize touch navigation when DOM is loaded
|
||||
// Initialize touch navigation when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new TouchNavigation();
|
||||
|
||||
// Update current section on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.touchNav) {
|
||||
window.touchNav.updateCurrentSection();
|
||||
}
|
||||
}, { passive: true });
|
||||
});
|
||||
|
||||
@@ -5,24 +5,176 @@
|
||||
'use strict';
|
||||
|
||||
function sendToAnalytics(metric) {
|
||||
const safeEntries = Array.isArray(metric.entries) ? metric.entries : [];
|
||||
const safeValue = Number.isFinite(metric.value) ? Math.round(metric.value) : 0;
|
||||
const safeDelta = Number.isFinite(metric.delta) ? Math.round(metric.delta) : 0;
|
||||
|
||||
// In production we log to console. The /api/web-vitals endpoint was removed
|
||||
// to reduce unnecessary network noise as we are not running a telemetry backend.
|
||||
console.log(`[Web Vitals] ${metric.name}: ${safeValue}`);
|
||||
}
|
||||
|
||||
// Load web-vitals from CDN
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js';
|
||||
script.onload = function() {
|
||||
if (window.webVitals) {
|
||||
window.webVitals.onCLS(sendToAnalytics);
|
||||
window.webVitals.onFID(sendToAnalytics);
|
||||
window.webVitals.onLCP(sendToAnalytics);
|
||||
window.webVitals.onFCP(sendToAnalytics);
|
||||
window.webVitals.onTTFB(sendToAnalytics);
|
||||
const data = {
|
||||
name: metric.name,
|
||||
value: safeValue,
|
||||
id: metric.id || 'unknown',
|
||||
delta: safeDelta,
|
||||
entries: safeEntries.map(e => ({
|
||||
name: e.name,
|
||||
startTime: e.startTime,
|
||||
duration: e.duration
|
||||
})),
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const payload = JSON.stringify(data);
|
||||
|
||||
// Send to analytics endpoint
|
||||
if (navigator.sendBeacon) {
|
||||
const blob = new Blob([payload], { type: 'application/json' });
|
||||
const ok = navigator.sendBeacon('/api/web-vitals', blob);
|
||||
if (!ok) {
|
||||
fetch('/api/web-vitals', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: payload,
|
||||
keepalive: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
fetch('/api/web-vitals', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: payload,
|
||||
keepalive: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Also log to console in development
|
||||
console.log(`[Web Vitals] ${metric.name}: ${metric.value}`, metric);
|
||||
}
|
||||
|
||||
// Largest Contentful Paint (LCP)
|
||||
function observeLCP() {
|
||||
try {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
sendToAnalytics({
|
||||
name: 'LCP',
|
||||
value: lastEntry.renderTime || lastEntry.loadTime,
|
||||
id: lastEntry.id,
|
||||
delta: 0,
|
||||
entries: entries
|
||||
});
|
||||
});
|
||||
observer.observe({ entryTypes: ['largest-contentful-paint'] });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// First Input Delay (FID)
|
||||
function observeFID() {
|
||||
try {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach(entry => {
|
||||
sendToAnalytics({
|
||||
name: 'FID',
|
||||
value: entry.processingStart - entry.startTime,
|
||||
id: entry.id,
|
||||
delta: 0,
|
||||
entries: [entry]
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe({ entryTypes: ['first-input'] });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Cumulative Layout Shift (CLS)
|
||||
function observeCLS() {
|
||||
try {
|
||||
let clsValue = 0;
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
try {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach(entry => {
|
||||
if (!entry.hadRecentInput) {
|
||||
clsValue += entry.value;
|
||||
}
|
||||
});
|
||||
sendToAnalytics({
|
||||
name: 'CLS',
|
||||
value: clsValue,
|
||||
id: 'cls',
|
||||
delta: 0,
|
||||
entries: entries
|
||||
});
|
||||
} catch (e) {
|
||||
// CLS measurement failed, but don't crash the whole script
|
||||
console.log('CLS measurement error:', e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Try to observe layout-shift, but handle if it's not supported
|
||||
try {
|
||||
observer.observe({ entryTypes: ['layout-shift'] });
|
||||
} catch (e) {
|
||||
console.log('Layout-shift observation not supported:', e.message);
|
||||
// CLS will not be measured, but other metrics will still work
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('CLS observer setup failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Time to First Byte (TTFB)
|
||||
function measureTTFB() {
|
||||
try {
|
||||
const nav = performance.getEntriesByType('navigation')[0];
|
||||
if (nav) {
|
||||
sendToAnalytics({
|
||||
name: 'TTFB',
|
||||
value: nav.responseStart,
|
||||
id: 'ttfb',
|
||||
delta: 0,
|
||||
entries: [nav]
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// First Contentful Paint (FCP)
|
||||
function observeFCP() {
|
||||
try {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach(entry => {
|
||||
if (entry.name === 'first-contentful-paint') {
|
||||
sendToAnalytics({
|
||||
name: 'FCP',
|
||||
value: entry.startTime,
|
||||
id: entry.id,
|
||||
delta: 0,
|
||||
entries: [entry]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe({ entryTypes: ['paint'] });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
if (document.readyState === 'complete') {
|
||||
observeLCP();
|
||||
observeFID();
|
||||
observeCLS();
|
||||
observeFCP();
|
||||
measureTTFB();
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
observeLCP();
|
||||
observeFID();
|
||||
observeCLS();
|
||||
observeFCP();
|
||||
measureTTFB();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user