|
|
|
|
|
const toggle = document.getElementById('themeToggle'); |
|
|
const root = document.documentElement; |
|
|
const saved = localStorage.getItem('theme') || 'dark'; |
|
|
|
|
|
|
|
|
if (saved === 'light') { |
|
|
document.documentElement.classList.remove('dark'); |
|
|
} else { |
|
|
document.documentElement.classList.add('dark'); |
|
|
} |
|
|
|
|
|
|
|
|
const updateThemeIcon = () => { |
|
|
const icon = toggle?.querySelector('i'); |
|
|
if (icon) { |
|
|
icon.setAttribute('data-lucide', document.documentElement.classList.contains('dark') ? 'sun-medium' : 'moon'); |
|
|
if (window.lucide) { |
|
|
lucide.createIcons(); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
updateThemeIcon(); |
|
|
|
|
|
toggle?.addEventListener('click', () => { |
|
|
const isDark = document.documentElement.classList.toggle('dark'); |
|
|
localStorage.setItem('theme', isDark ? 'dark' : 'light'); |
|
|
updateThemeIcon(); |
|
|
|
|
|
|
|
|
document.body.style.transition = 'background-color 0.3s ease, color 0.3s ease'; |
|
|
setTimeout(() => { |
|
|
document.body.style.transition = ''; |
|
|
}, 300); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab').forEach(btn => { |
|
|
btn.addEventListener('click', () => { |
|
|
|
|
|
document.querySelectorAll('.tab').forEach(b => { |
|
|
b.classList.remove('active'); |
|
|
b.setAttribute('aria-selected', 'false'); |
|
|
}); |
|
|
|
|
|
|
|
|
btn.classList.add('active'); |
|
|
btn.setAttribute('aria-selected', 'true'); |
|
|
|
|
|
|
|
|
const target = btn.getAttribute('data-target'); |
|
|
const targetDiagram = document.querySelector(target); |
|
|
|
|
|
if (targetDiagram) { |
|
|
|
|
|
document.querySelectorAll('.diagram').forEach(d => { |
|
|
d.style.opacity = '0'; |
|
|
d.style.transform = 'translateY(20px)'; |
|
|
d.style.display = 'none'; |
|
|
d.setAttribute('aria-hidden', 'true'); |
|
|
d.classList.remove('visible'); |
|
|
}); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
targetDiagram.style.display = 'block'; |
|
|
targetDiagram.classList.add('visible'); |
|
|
targetDiagram.style.opacity = '1'; |
|
|
targetDiagram.style.transform = 'translateY(0)'; |
|
|
targetDiagram.setAttribute('aria-hidden', 'false'); |
|
|
}, 150); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
document.querySelectorAll('.diagram').forEach((diagram, index) => { |
|
|
if (index === 0) { |
|
|
|
|
|
diagram.style.display = 'block'; |
|
|
diagram.style.opacity = '1'; |
|
|
diagram.style.transform = 'translateY(0)'; |
|
|
diagram.classList.add('visible'); |
|
|
diagram.setAttribute('aria-hidden', 'false'); |
|
|
} else { |
|
|
|
|
|
diagram.style.display = 'none'; |
|
|
diagram.style.opacity = '0'; |
|
|
diagram.style.transform = 'translateY(20px)'; |
|
|
diagram.classList.remove('visible'); |
|
|
diagram.setAttribute('aria-hidden', 'true'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (window.mermaid) { |
|
|
mermaid.init(undefined, document.querySelectorAll('.mermaid')); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const counters = document.querySelectorAll('.metric-value'); |
|
|
const easeOutCubic = t => 1 - Math.pow(1 - t, 3); |
|
|
const easeOutQuart = t => 1 - Math.pow(1 - t, 4); |
|
|
|
|
|
const animateCount = (el, to, duration = 1200) => { |
|
|
const start = performance.now(); |
|
|
const from = 0; |
|
|
const isLargeNumber = to > 1000; |
|
|
const easing = isLargeNumber ? easeOutQuart : easeOutCubic; |
|
|
|
|
|
|
|
|
el.style.position = 'relative'; |
|
|
el.style.zIndex = '2'; |
|
|
el.style.width = '100%'; |
|
|
el.style.textAlign = 'center'; |
|
|
el.style.display = 'block'; |
|
|
|
|
|
const step = (now) => { |
|
|
const p = Math.min(1, (now - start) / duration); |
|
|
const v = Math.floor(from + (to - from) * easing(p)); |
|
|
|
|
|
|
|
|
if (to >= 1000000) { |
|
|
el.textContent = (v / 1000000).toFixed(1) + 'M'; |
|
|
} else if (to >= 1000) { |
|
|
el.textContent = (v / 1000).toFixed(1) + 'K'; |
|
|
} else { |
|
|
el.textContent = v.toLocaleString(); |
|
|
} |
|
|
|
|
|
if (p < 1) { |
|
|
requestAnimationFrame(step); |
|
|
} else { |
|
|
|
|
|
if (to >= 1000000) { |
|
|
el.textContent = (to / 1000000).toFixed(1) + 'M'; |
|
|
} else if (to >= 1000) { |
|
|
el.textContent = (to / 1000).toFixed(1) + 'K'; |
|
|
} else { |
|
|
el.textContent = to.toLocaleString(); |
|
|
} |
|
|
} |
|
|
}; |
|
|
requestAnimationFrame(step); |
|
|
}; |
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver(entries => { |
|
|
entries.forEach(e => { |
|
|
if (e.isIntersecting) { |
|
|
const el = e.target; |
|
|
const count = parseInt(el.dataset.count, 10) || 0; |
|
|
|
|
|
|
|
|
el.classList.add('loading'); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
animateCount(el, count); |
|
|
el.classList.remove('loading'); |
|
|
}, 200); |
|
|
|
|
|
observer.unobserve(el); |
|
|
} |
|
|
}); |
|
|
}, { |
|
|
threshold: 0.3, |
|
|
rootMargin: '0px 0px -50px 0px' |
|
|
}); |
|
|
|
|
|
|
|
|
counters.forEach(c => observer.observe(c)); |
|
|
|
|
|
|
|
|
const initTilt = () => { |
|
|
if (window.VanillaTilt) { |
|
|
try { |
|
|
document.querySelectorAll('[data-tilt]').forEach(el => { |
|
|
VanillaTilt.init(el, { |
|
|
max: 6, |
|
|
speed: 400, |
|
|
glare: true, |
|
|
'max-glare': 0.1, |
|
|
scale: 1.02, |
|
|
gyroscope: false |
|
|
}); |
|
|
}); |
|
|
} catch (error) { |
|
|
console.warn('VanillaTilt initialization failed:', error); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
initTilt(); |
|
|
|
|
|
|
|
|
document.querySelectorAll('a[href^="#"]').forEach(link => { |
|
|
link.addEventListener('click', (e) => { |
|
|
e.preventDefault(); |
|
|
const targetId = link.getAttribute('href'); |
|
|
const targetElement = document.querySelector(targetId); |
|
|
|
|
|
if (targetElement) { |
|
|
const headerHeight = document.querySelector('header')?.offsetHeight || 0; |
|
|
const targetPosition = targetElement.offsetTop - headerHeight - 20; |
|
|
|
|
|
window.scrollTo({ |
|
|
top: targetPosition, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const handleResourceError = (resource, fallback) => { |
|
|
resource.addEventListener('error', () => { |
|
|
console.warn(`Failed to load resource, using fallback`); |
|
|
if (fallback) { |
|
|
resource.src = fallback; |
|
|
} |
|
|
}); |
|
|
}; |
|
|
|
|
|
|
|
|
window.addEventListener('error', (e) => { |
|
|
if (e.target.tagName === 'SCRIPT' && e.target.src) { |
|
|
console.warn(`Script failed to load: ${e.target.src}`); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const logPerformance = () => { |
|
|
if ('performance' in window) { |
|
|
window.addEventListener('load', () => { |
|
|
setTimeout(() => { |
|
|
const perfData = performance.getEntriesByType('navigation')[0]; |
|
|
console.log(`Page load time: ${perfData.loadEventEnd - perfData.loadEventStart}ms`); |
|
|
}, 0); |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
logPerformance(); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
|
|
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { |
|
|
const activeTab = document.querySelector('.tab.active'); |
|
|
if (activeTab) { |
|
|
const tabs = Array.from(document.querySelectorAll('.tab')); |
|
|
const currentIndex = tabs.indexOf(activeTab); |
|
|
let nextIndex; |
|
|
|
|
|
if (e.key === 'ArrowLeft') { |
|
|
nextIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1; |
|
|
} else { |
|
|
nextIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0; |
|
|
} |
|
|
|
|
|
tabs[nextIndex].click(); |
|
|
tabs[nextIndex].focus(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const addLoadingState = (element, duration = 1000) => { |
|
|
element.classList.add('loading'); |
|
|
setTimeout(() => { |
|
|
element.classList.remove('loading'); |
|
|
}, duration); |
|
|
}; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
document.querySelectorAll('.card').forEach(card => { |
|
|
card.addEventListener('mouseenter', () => { |
|
|
addLoadingState(card, 500); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.chip').forEach(chip => { |
|
|
chip.setAttribute('title', chip.textContent); |
|
|
}); |
|
|
}); |
|
|
|