Spaces:
Sleeping
Sleeping
| // Enhanced theme toggle with smooth transitions | |
| const toggle = document.getElementById('themeToggle'); | |
| const root = document.documentElement; | |
| const saved = localStorage.getItem('theme') || 'dark'; | |
| // Apply saved theme | |
| if (saved === 'light') { | |
| document.documentElement.classList.remove('dark'); | |
| } else { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| // Update theme toggle icon | |
| 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(); | |
| } | |
| } | |
| }; | |
| // Initialize theme icon | |
| updateThemeIcon(); | |
| toggle?.addEventListener('click', () => { | |
| const isDark = document.documentElement.classList.toggle('dark'); | |
| localStorage.setItem('theme', isDark ? 'dark' : 'light'); | |
| updateThemeIcon(); | |
| // Add smooth transition effect | |
| document.body.style.transition = 'background-color 0.3s ease, color 0.3s ease'; | |
| setTimeout(() => { | |
| document.body.style.transition = ''; | |
| }, 300); | |
| }); | |
| // Enhanced tabs logic with smooth transitions and ARIA support | |
| document.querySelectorAll('.tab').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| // Remove active class and ARIA attributes from all tabs | |
| document.querySelectorAll('.tab').forEach(b => { | |
| b.classList.remove('active'); | |
| b.setAttribute('aria-selected', 'false'); | |
| }); | |
| // Add active class and ARIA attributes to clicked tab | |
| btn.classList.add('active'); | |
| btn.setAttribute('aria-selected', 'true'); | |
| // Get target diagram | |
| const target = btn.getAttribute('data-target'); | |
| const targetDiagram = document.querySelector(target); | |
| if (targetDiagram) { | |
| // Hide all diagrams completely and update ARIA | |
| 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'); | |
| }); | |
| // Show target diagram with fade in and update ARIA | |
| setTimeout(() => { | |
| targetDiagram.style.display = 'block'; | |
| targetDiagram.classList.add('visible'); | |
| targetDiagram.style.opacity = '1'; | |
| targetDiagram.style.transform = 'translateY(0)'; | |
| targetDiagram.setAttribute('aria-hidden', 'false'); | |
| }, 150); | |
| } | |
| }); | |
| }); | |
| // Initialize first tab as active and show first diagram | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Hide all diagrams except the first one | |
| document.querySelectorAll('.diagram').forEach((diagram, index) => { | |
| if (index === 0) { | |
| // First diagram should be visible | |
| diagram.style.display = 'block'; | |
| diagram.style.opacity = '1'; | |
| diagram.style.transform = 'translateY(0)'; | |
| diagram.classList.add('visible'); | |
| diagram.setAttribute('aria-hidden', 'false'); | |
| } else { | |
| // Other diagrams should be hidden | |
| diagram.style.display = 'none'; | |
| diagram.style.opacity = '0'; | |
| diagram.style.transform = 'translateY(20px)'; | |
| diagram.classList.remove('visible'); | |
| diagram.setAttribute('aria-hidden', 'true'); | |
| } | |
| }); | |
| // Ensure Mermaid diagrams are properly initialized | |
| if (window.mermaid) { | |
| mermaid.init(undefined, document.querySelectorAll('.mermaid')); | |
| } | |
| }); | |
| // Enhanced metric counters with better performance | |
| 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; | |
| // Ensure the element has proper positioning | |
| 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)); | |
| // Format number with appropriate separators | |
| 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 { | |
| // Ensure final value is exact | |
| 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); | |
| }; | |
| // Enhanced intersection observer with better performance | |
| const observer = new IntersectionObserver(entries => { | |
| entries.forEach(e => { | |
| if (e.isIntersecting) { | |
| const el = e.target; | |
| const count = parseInt(el.dataset.count, 10) || 0; | |
| // Add loading state | |
| el.classList.add('loading'); | |
| // Animate with slight delay for better visual effect | |
| setTimeout(() => { | |
| animateCount(el, count); | |
| el.classList.remove('loading'); | |
| }, 200); | |
| observer.unobserve(el); | |
| } | |
| }); | |
| }, { | |
| threshold: 0.3, | |
| rootMargin: '0px 0px -50px 0px' | |
| }); | |
| // Observe counters | |
| counters.forEach(c => observer.observe(c)); | |
| // Enhanced VanillaTilt initialization with error handling | |
| 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); | |
| } | |
| } | |
| }; | |
| // Initialize tilt effects | |
| initTilt(); | |
| // Smooth scrolling for navigation links | |
| 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' | |
| }); | |
| } | |
| }); | |
| }); | |
| // Enhanced error handling for external resources | |
| const handleResourceError = (resource, fallback) => { | |
| resource.addEventListener('error', () => { | |
| console.warn(`Failed to load resource, using fallback`); | |
| if (fallback) { | |
| resource.src = fallback; | |
| } | |
| }); | |
| }; | |
| // Handle external script loading errors | |
| window.addEventListener('error', (e) => { | |
| if (e.target.tagName === 'SCRIPT' && e.target.src) { | |
| console.warn(`Script failed to load: ${e.target.src}`); | |
| } | |
| }); | |
| // Performance monitoring | |
| 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); | |
| }); | |
| } | |
| }; | |
| // Initialize performance monitoring | |
| logPerformance(); | |
| // Add keyboard navigation support | |
| document.addEventListener('keydown', (e) => { | |
| // Tab navigation for architecture tabs | |
| 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(); | |
| } | |
| } | |
| }); | |
| // Add loading states for dynamic content | |
| const addLoadingState = (element, duration = 1000) => { | |
| element.classList.add('loading'); | |
| setTimeout(() => { | |
| element.classList.remove('loading'); | |
| }, duration); | |
| }; | |
| // Initialize all interactive elements | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Add loading states to cards that might load content dynamically | |
| document.querySelectorAll('.card').forEach(card => { | |
| card.addEventListener('mouseenter', () => { | |
| addLoadingState(card, 500); | |
| }); | |
| }); | |
| // Initialize tooltips for chips | |
| document.querySelectorAll('.chip').forEach(chip => { | |
| chip.setAttribute('title', chip.textContent); | |
| }); | |
| }); | |